diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e2804ef..1ffe801 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,11 @@ jobs: strategy: fail-fast: false matrix: - image: [Pharo64-13, Moose64-13] + include: + - image: Pharo64-13 + config: .smalltalk.ston + - image: Moose64-13 + config: .smalltalk.moose.ston services: postgres: image: postgres:15 @@ -43,7 +47,7 @@ jobs: with: smalltalk-image: ${{ matrix.image }} - name: Load and test in a clean image - run: smalltalkci -s '${{ matrix.image }}' .smalltalk.ston + run: smalltalkci -s '${{ matrix.image }}' '${{ matrix.config }}' - name: Upload JUnit results if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.smalltalk.moose.ston b/.smalltalk.moose.ston new file mode 100644 index 0000000..52f6c33 --- /dev/null +++ b/.smalltalk.moose.ston @@ -0,0 +1,22 @@ +SmalltalkCISpec { + #name : 'FAMIXNGSQL-2-Pharo13-Moose13', + #preLoading : 'scripts/ci/prepare-moose.st', + #loading : [ + SCIMetacelloLoadSpec { + #baseline : 'FAMIXNGSQL', + #directory : 'src', + #load : [ 'Tests', 'GeneratorTests', 'MooseIDETests' ], + #onConflict : #useIncoming, + #onUpgrade : #useIncoming, + #platforms : [ #pharo ] + } + ], + #postLoading : 'scripts/ci/load-famix-regressions.st', + #preTesting : 'scripts/ci/configure-database.st', + #testing : { + #packages : [ 'FamixNGSQL-Importer-Tests', 'FAMIXNGSQLMetamodelGenerator-Tests', 'PgMetadata-tests', 'FamixNGSQL-MooseIDE-Tests' ], + #classes : [ #MiMetaBrowserRegressionTest, #MiModelReportRegressionTest, #MiLayerApplicabilityTest, #MiApplicationTest, #MiButterflyMapModelTest, #MiButterflyMapBrowserTest, #MooseGroupNameCloudTest, #MQNavigationContainmentTest, #FamixNamespaceGroupHierarchyTest, #MooseGroupTest ], + #defaultTimeout : 120, + #failOnZeroTests : true + } +} diff --git a/README.md b/README.md index fa137cf..7fc4243 100644 --- a/README.md +++ b/README.md @@ -78,9 +78,10 @@ immutable Moose 13 release. smalltalkCI selects it with `Moose64-13`. | `Tests` | Core, importer tests and PgMetadata scenario-test support | | `Generator` | Core and the metamodel generator | | `GeneratorTests` | Generator plus its global-regeneration selection regression test | -| `LegacyUI` | Historical GT Inspector, analysis, Telescope and connection-manager packages | +| `MooseIDE` | Optional standard Moose tool adapters and Roassal 3 SQL views (requires Moose 13) | +| `MooseIDETests` | Optional tool and visualization regression tests | -`LegacyUI` has not been ported or validated on the current image. Core usage does +The unsupported `LegacyUI` load group is removed for v3. Historical sources remain archived in `src`; use the [Roassal 3 SQL views](docs/mooseide-integration.md) instead. Core usage does not require those UI packages or a Genie MCP server. Loading modern Genie in a historical Pharo 7 image is not part of the installation procedure. @@ -487,3 +488,7 @@ export can be parsed. Distributed under the [MIT license](LICENSE). This fork builds on [Julien Delplanque's FAMIXNGSQL](https://github.com/juliendelplanque/FAMIXNGSQL), with updated metadata extraction, SQL parsing and Moose metamodel integration. + +## v3.0.0 qualification + +See the [release plan and issue/PR map](docs/v3-release-plan.md) for the compatibility audit, separate fixes, breaking changes and release gates. This branch is an integration candidate; v3.0.0 is not released. diff --git a/docs/moose-compatibility-audit.md b/docs/moose-compatibility-audit.md new file mode 100644 index 0000000..6cfd72c --- /dev/null +++ b/docs/moose-compatibility-audit.md @@ -0,0 +1,334 @@ +# SQL model compatibility with Moose and Roassal + +Audit date: 2026-09-20. Repository: `deem0n/FAMIXNGSQL`, master at +`6e1c636943796e1abba5c4a92b6e7270a6e2b00f`. + +The SQL model works with generic inspection and several standard visualizations, +but it is not yet compatible with every installed Moose analysis. The most +important problems are recursive dependency traversal, non-unique entity names, +and incomplete source-anchor protocols. Some browsers also assume an object-oriented +model even though their acceptance checks admit SQL entities. + +## Environment and scope + +- Running Pharo 13.1.0 snapshot / Moose 13 image, accessed through Genie. +- Existing PostgreSQL model `mi`: 70,144 entities, 53 concrete SQL entity classes + represented, 49 namespaces including catalog namespaces; language PostgreSQL. +- Inventoried all 28 installed concrete subclasses of `MiAbstractBrowser`. +- Exercised 509 supported core operations across one representative of each of + the 53 entity classes, in addition to the initial 143-operation targeted pass. +- Checked applicable Famix visualizations and built three new Roassal 3 examples. +- Used isolated models for destructive-analysis entry points such as duplication, + and Smalltalk/Java controls where useful. The live model was not reimported. +- The final entity count and identity set match the pre-audit model. No production + methods were changed, no image was saved, and nothing was committed or pushed. + +This is an audit of the installed image, not every Moose plugin or every possible +UI interaction. The image already contains earlier namespace-hierarchy and Fame +inspection fixes, as well as previous SQL migration work. A successful browser +open is only an entry-point smoke test unless deeper validation is stated below. +No full-model performance or parser-completeness claim is made. + +Local evidence is in the ignored `artifacts/` directory: + +- `moose-tool-inventory.json`: browser classes, packages and acceptance/follow methods. +- `moose-compatibility-results.json`: individual probes, observations and failure stacks. +- `audit-sql-roassal.st`: executable examples; three corresponding PNG exports. +- `audit-moose-reproducers.st`: isolated, read-only diagnostic reproductions. + +Probe `pass` means the observation completed, not that the observed behavior is +correct. For example, a rejected input, an empty visualization and a wrong +dead-code classification can all be successfully recorded observations. One early +Bus Log construction failure was a harness setup error: the correct bus/application +factory subsequently opened successfully. It is not classified as a product bug. + +## Confirmed problems and proposed fixes + +### 1. Recursive navigation assumes all child relations are collections + +**Priority: high. Owner: Famix / MooseQuery.** + +`MQNavigationQuery>>queryFor:` passes each child property to +`OrderedCollection withAll:`. A SQL view's `sqlQuery` is a single entity, so the +query raises `Error: Instances of FmxSQLSelectQuery are not indexable`. + +The broad 509-operation pass had 16 failures: `queryAllIncoming`, +`queryAllOutgoing`, `allClients` and `allProviders` on each sampled view, derived +table, namespace and SELECT query. Enclosing entities fail when traversal reaches +a nested scalar query relation. A simple SQL table can therefore work while a +view or an enclosing schema fails. + +Reproducer, independent of the database: + +```smalltalk +| model view query | +model := FmxSQLModel new. +query := FmxSQLSelectQuery new mooseModel: model; yourself. +view := FmxSQLView new + name: 'audit_view'; sqlQuery: query; mooseModel: model; yourself. +view queryAllOutgoing. "Currently raises the not-indexable error" +``` + +Confirmed consumers: view-based DSM, view-based Butterfly, and the CoUsage provider +extractor. Query-browser recursive operations share this path even though opening +the query browser succeeds. The architectural map uses different/local navigation +paths and can still render. + +**Fix:** make upstream traversal respect relation cardinality, preferably using the +existing scalar-aware `containedEntitiesDo:` protocol. Retain SQL's legitimate +one-to-one `sqlQuery` relation. Add scalar/many-valued containment regression cases +and check shared containment for duplicate work or cycles before generalizing the +implementation. Changing `sqlQuery` back to `query` would reintroduce the earlier +Moose query API collision and would not fix this problem. + +### 2. SQL names violate the Moose uniqueness contract + +**Priority: high. Owner: FAMIXNGSQL.** + +SQL named entities inherit `mooseNameOn:` that writes only `name`, while +`hasUniqueMooseNameInModel` returns true. Tables with the same name in different +schemas therefore have identical Moose names. Java and Smalltalk class naming +implementations include their containers. + +An isolated model with `audit_one.same_table` and `audit_two.same_table` produces +`same_table` for both entities. `model entityNamed: 'same_table'` returns the first +one, and lookup using either qualified name returns nil. This is a name-lookup +ambiguity; entity identities remain distinct. + +Observed collisions among nonstub entities in `mi`: + +| Entity type | Entities | Colliding name groups | Entities in those groups | +| --- | ---: | ---: | ---: | +| Table | 359 | 56 | 218 | +| View | 85 | 14 | 57 | +| Column | 4,221 | 395 | 3,925 | +| Stored procedure | 2,321 | 342 | 1,174 | +| Trigger stored procedure | 140 | 1 | 2 | +| Trigger | 569 | 60 | 256 | + +**Fix:** define stable SQL-qualified Moose names. Include schemas for relations, +relation/routine owners for contained names, and input type signatures for +overloaded routines. Define quoting/escaping rules for PostgreSQL identifiers and +invalidate cached names when their components change. Test lookup across schemas, +overloads, quoted identifiers, reparenting and MSE round trips. For entities without +a meaningful unique name, use an honest uniqueness contract rather than claiming +that a short display label is globally unique. + +The Roassal examples qualify labels explicitly as a presentation workaround; they +do not change model naming or lookup behavior. + +### 3. SQL source anchors leave required methods unimplemented + +**Priority: high. Owner: FAMIXNGSQL.** + +`FmxSQLEntitySourceAnchor` inherits explicit requirements for `knowsStart`, +`knowsEnd` and `hasSource`. All three raise `Error: Explicitly required method`. +This was the only represented SQL class with unresolved explicit trait requirements +in the static method scan. + +The Duplication browser accepts anchored SQL query entities, but its detector +calls `knowsStart`/`knowsEnd` during input filtering. Running duplication on two +isolated SQL query fixtures reproduced the failure, without modifying `mi`. + +**Fix:** implement the full anchor protocol with accurate bounds semantics, using +the existing owner-source/start/end representation. Test valid, missing, partial +and out-of-range anchors. Then exercise the detector and fragment display further; +passing the first bounds checks alone will not prove complete duplication support. + +### 4. Source Text browser cannot adapt SQL sources + +**Priority: high. Owner: SQL/MooseIDE integration.** + +`MiSourceTextBrowser` accepts routines and views because they use +`FamixTSourceEntity`, then fails with `No source anchor adpater found` while building +highlights. The installed adapters recognize Smalltalk entities or indexed-file +anchors. SQL stores full definitions and source intervals inside model entities. + +Full source text is present: all 85 views expose their stored definition through +`sourceText`. However, none of those views has a conventional `sourceAnchor`. +Of 5,556 exact stored-procedure entities including stubs, only 9 have an anchor; +all 1,481 SELECT query entities have an entity source anchor. + +**Fix:** add an adapter for immediate/owner-backed source text, with correct +highlight offsets and a plain-text fallback when ranges are unavailable. Keep +MooseIDE-specific integration in a suitable optional package. Do not fabricate +filesystem anchors for definitions read from PostgreSQL. + +### 5. Default analyses have object-oriented semantics + +**Priority: medium, high if used to make removal decisions. Owners: SQL integration +and MooseIDE/Famix analysis packages.** + +- **CoUsage:** the default attribute-access extractor sends `#accesses` to SQL + tables, which do not implement it. `MiCoUsageMapModel>>allAttributesFor:` catches + the error and returns an empty collection. The actual browser showed three + container boxes and zero inner boxes. Add a SQL extractor for table/column or + routine/reference co-usage, with explicit applicability and visible failures. +- **Dead Code:** the default rule checks only `incomingInvocations isEmpty`. + Full analysis on an isolated routine bound to a trigger classified it as dead. + Trigger bindings, external entry points, scheduled calls and unresolved/dynamic + SQL need explicit treatment. Such results should be candidates requiring review, + not proof that database routines are unused. +- **Duplication cleaner:** the default is `FamixRepCLikeCleaner`. A probe retained + `-- SQL comment` but truncated `SELECT 'https://example.test';` to + `SELECT'https:`. Add SQL-aware token/string/comment handling; changing the + source-anchor protocol alone will not make comparison meaningful. +- **Layer Visualization:** accepts the model, but + `MiLayerVisualizationModel findModelApplicableTo:` returns nil. Add a SQL layer + strategy or reject the model with an explicit explanation. + +### 6. Model Report divides by zero on a non-OO model + +**Priority: medium. Owner: MooseIDE-Core-Reporter.** + +`MiModelReportModel>>sectionPackages` divides the count of OO classes by the count +of OO packages. Both are zero in this SQL model. Opening the accepted model raises +`ZeroDivide`. + +**Fix:** handle empty denominators and make report sections conditional on supported +model capabilities. Add SQL sections for schemas, relations, routines, triggers, +constraints, dependency coverage and analysis diagnostics. SQL schemas should not +be represented as OO packages solely to satisfy this calculation. + +### 7. Meta Browser overwrites a framework property dictionary + +**Priority: medium. Owner: MooseIDE-Meta. Not SQL-specific.** + +`MiMetaBrowser>>initializeProperties` assigns a notebook presenter to the inherited +`properties` slot. `SpPresenter>>hasInitialPosition` subsequently sends `keys` to +that notebook. Opening fails with `SpNotebookPresenter did not understand #keys`. +A fresh `FamixStModel` reproduced the same failure. + +**Fix:** give the notebook its own distinctly named instance variable and update +the browser's references, preserving the presenter's property dictionary. Cover +normal window construction as well as SQL and Smalltalk model reception. + +### 8. Name Cloud has missing class-side helpers + +**Priority: medium. Owner: Famix-Visualizations. Not SQL-specific.** + +`MooseGroupNameCloud>>entityAndItsKeywords:` sends `self class separators`, which +does not exist in this image. Both SQL tables and a synthetic Smalltalk class fail +with `MooseGroupNameCloud class did not understand #separators`. + +**Fix:** restore the tokenizer configuration expected by the visualization and test +the complete keyword pipeline, including stop-word handling. SQL-specific name +tokenization can be added after the generic failure is fixed. + +### 9. Legacy SQL visualization packages are still unported + +**Priority: medium if these optional packages are to be supported. Owner: FAMIXNGSQL.** + +The core baseline intentionally excludes these legacy packages. Static inspection +found obsolete dependencies and stale SQL selectors: + +- `FAMIXNGSQLAnalysis`: `RTMondrian`, `RTArrowedLine`, `RTLegendBuilder`. +- `FamixNGSQL-GTInspectorExtension`: old GT/Glamour APIs and `GTObjectPrinter`. +- `FmxSQLFKDependenciesAnalyzer>>analyze` calls `referencedColumns`. A real current + foreign-key constraint does not implement it. The current relation is + `outgoingForeignKeyColumnReferences`, whose targets are columns. +- Other legacy code references `foreignKeyConstraints`, `referencedColumnsTable` + and old concept-tag selectors and needs a separate porting review. + +The Roassal 2/GT classes above are absent in the running image. These optional +packages were not loaded during the audit. Port them to Roassal 3 and current +relation APIs before advertising them as supported. + +### 10. Previously identified provider-direction problem remains relevant + +The installed `MiApplication>>defaultProvidersQuery` still uses `allClients`. +The existing user Butterfly window has an instance-level corrected query. This is +a separate known upstream change, represented by MooseIDE PR #1659; it was not +newly introduced or fixed by this audit. New windows must not be assumed to inherit +the existing window's override. + +## Installed browser coverage + +| Browser | Observed result | Scope or limitation | +| --- | --- | --- | +| Aggregator | Open/follow succeeds | Three-table input | +| Architectural Map | Renders | Tables and views; existing standard schema map has 47 nodes / 115 edges | +| Butterfly Map | Table renders; view fails | Table sample 5 nodes / 4 edges; recursive traversal blocker on view; default provider-direction issue also applies | +| Class Blueprint | SQL type rejected | Requires `FamixTType`; no SQL blueprint adapter | +| CoUsage Map | Opens with empty inner contents | Confirmed swallowed extractor error, three containers / zero inner boxes | +| Critic | Open/follow succeeds | No SQL rule suite executed | +| Dead Code | Opens; isolated analysis runs | Trigger-bound routine misclassified as dead | +| Distribution Map | Renders tables and views | Three-node samples; no tags assigned during audit, so no substantive tag distribution validated | +| DSM | Tables work; views fail | Recursive traversal blocker | +| Duplication | Routine without anchor rejected; anchored queries accepted | Actual isolated computation fails required anchor protocol | +| Export | Open/follow succeeds | Export action/file writing not exercised by this audit | +| Files | Model rejected | SQL catalog model has no source-tree `rootFolder` | +| Heat Map | Renders tables and views | Default metric is constant 1; SQL column-count metric also rendered with values 34, 8, 8 | +| Inspector | Open/follow succeeds | Fame regression checks also pass for SQL, Smalltalk and Java in this patched image | +| Layer Visualization | Opens without a visualization model | No applicable SQL implementation | +| Meta Browser | Fails on opening | Notebook/property collision, also reproduced with Smalltalk | +| Model Report | Fails | Division by zero in OO package section | +| Models Browser | Standalone window opens | Does not follow bus entities by design | +| Notebook | Open/follow succeeds | Model binding created; arbitrary notebook code/export not exercised | +| Playground | Open/follow succeeds | Entity binding created | +| Queries Browser | Root query opens | Recursive dependency actions subject to traversal failure | +| Queries Dashboard | Open/follow succeeds | No arbitrary dashboard widget suite exercised | +| Source Text | Routine and view fail | No applicable source adapter | +| System Complexity | No applicable SQL classes | Confirmed zero `isClass` entities; avoided its rejection dialog | +| Tag Browser | Open/follow succeeds | Tag mutation/deletion not exercised | +| UML | SQL type group rejected | Requires a `FamixTypeGroup`; SQL types do not supply OO class semantics | +| Logs | Standalone window opens | Does not follow bus entities directly | +| Bus Log | Configured window opens | Requires a bus/application factory | + +OO class inheritance, annotation constellations, package complexity, class nesting, +system attraction and overview-pyramid metrics are not automatically meaningful +for a relational model. Their absence is an applicability gap, not evidence that +SQL classes should blindly acquire OO traits. Appropriate analogues include +schema/table nesting, foreign-key graphs, routine-call graphs and SQL metrics. + +## Famix and Roassal visualizations exercised + +| Visualization | Result | +| --- | --- | +| Namespace Hierarchy | 49 nodes, 0 edges | +| Overall Namespace Hierarchy | 49 nodes, 0 edges | +| Fame property tables | SQL, Smalltalk and Java fixture checks pass | +| Fame class connections, SQL View | 7 nodes, 6 edges | +| SQL metamodel UML | 95 nodes, 94 edges | +| Name Cloud | Fails for SQL and Smalltalk controls | +| RSMondrian schema/table nesting | 8 schema composites containing 206 table boxes | +| RSMondrian foreign-key graph | 35 tables, 29 directed relationships | +| RSMondrian routine-call graph | 30 routines, 12 directed relationships | + +Zero edges in namespace hierarchy is expected for flat PostgreSQL schema +containment; this visualization is not a schema dependency graph. + +The new examples use the same `RSMondrian nodes:forEach:`, metric normalization, +line builders and layouts as the installed class/method examples. They use SQL's +actual containment and association APIs. Dependency lines are pushed to the front. +They are bounded samples for interactive inspection, not complete database graphs. +Routine edges retain candidate targets and do not prove runtime execution. + +The three example windows remain open in the running image. Reopen an existing +canvas from a Playground with, for example: + +```smalltalk +(FmxSQLAuditRoassalCanvases at: 'foreign-keys') + openWithTitle: 'SQL foreign-key dependencies'. +``` + +The other keys are `'nesting'` and `'routine-calls'`. To rebuild all examples, +evaluate `artifacts/audit-sql-roassal.st` in the image containing `MiFamixSQLModel`. +The script schedules UI work with `UIManager default defer:`; inspect +`FmxSQLAuditRoassalStatus` for completion. + +## Suggested implementation sequence + +1. Fix SQL naming and source-anchor requirements in FAMIXNGSQL, with focused + round-trip and cross-schema/overload tests. +2. Fix scalar containment traversal upstream in MooseQuery; verify DSM, Butterfly + and query consumers on views and enclosing schemas. +3. Add source-text support and SQL analysis strategies without making core model + loading depend on the UI. +4. Repair the generic Meta Browser, report denominator and Name Cloud defects + upstream, with Smalltalk/Java controls where applicable. +5. Port the optional legacy SQL visualizations or explicitly keep them unsupported. + +After these changes, rerun on a fresh supported Moose image as well as the current +image. Current success of patched inspector/hierarchy tools does not establish that +a clean installation already includes those upstream fixes. diff --git a/docs/mooseide-integration.md b/docs/mooseide-integration.md new file mode 100644 index 0000000..cf286fe --- /dev/null +++ b/docs/mooseide-integration.md @@ -0,0 +1,84 @@ +# Optional Moose 13 integration + +In a Moose 13 / Pharo 13 image with MooseIDE already installed, load the `MooseIDE` +group of FAMIXNGSQL. `MooseIDETests` adds its regression tests. The plain `Core` +group does not load these packages or require the Moose UI. The snippet pins the +review candidate; use `master` only after its prerequisite PRs are merged. + +```smalltalk +Metacello new + baseline: 'FAMIXNGSQL'; + repository: 'github://deem0n/FAMIXNGSQL:4a3e0dcb11e7ce06684ba19d4addb61976b96498/src'; + load: 'MooseIDE'. +``` + +The Source Text browser automatically selects the SQL adapter. Routine and view +sources display without filesystem anchors. AST highlights are translated from +owner-relative intervals into the displayed fragment; absent, invalid, unrelated +or outside-fragment intervals produce no highlight. Definitions are not modified. + +The dependency pin includes the cardinality-aware MooseQuery fix from +`deem0n/Famix` PR #2, required for views with scalar query containment. This is +fork-first v3 development work, not a published v3 release. + +GitHub CI loads these optional tests in the ready-made Moose image. The plain +Pharo job continues to test Core without GUI dependencies. + +## SQL CoUsage + +Select **SQL relation references** as the inner-box extractor in the standard +CoUsage map settings, or use: + +```smalltalk +FmxSQLCoUsageExtractor openOn: (model allWithType: FmxSQLStoredProcedure). +``` + +Containers are routines, views or tables; inner boxes are referenced database +tables/views. Column references are projected to their owning relation, and +foreign keys contribute referenced relations. Repeated references retain usage +counts. Empty results mean no modeled references, not proof that dynamic SQL has +no dependencies. The helper rejects mixed/unsupported input before opening. + +## Dead-code candidates + +Use `FmxSQLDeadCodeRule openOn: routines` to open the standard Dead Code browser +with the SQL refuting rule selected, or select that rule explicitly in an existing +browser before computing. SQL routines are externally callable, so this SQL preset +protects all of them until a closed-world candidate set is explicitly supplied. +The generic OO preset alone is not a SQL dead-code analysis. +Trigger/constraint bindings and missing or incomplete analysis remain protected. +For an explicitly bounded analysis, configure its selected `FmxSQLDeadCodeRule`: + +```smalltalk +rule closedWorldCandidates: knownClosedWorldRoutines. +rule externalEntryPoints: knownExternalRoots. +``` + +The remaining results are candidates, not proof that routines can be dropped. +This integration does not execute database DDL or remove definitions. Existing +non-SQL dead-code rules retain their behavior. + +## SQL duplication analysis + +Choose **PostgreSQL** as the source cleaner in the standard Duplication Browser settings, or open it with the preset: + +```smalltalk +FmxSQLReplicationCleaner openOn: (model entities select: [ :entity | + entity hasSourceAnchor and: [ entity sourceAnchor hasSourceText ] ]). +``` + +The cleaner preserves SQL literals, quoted identifiers and dollar-quoted bodies; removes line and nested block comments; and keeps original line numbers. Whitespace inside literals stays significant. It assumes PostgreSQL `standard_conforming_strings = on`; explicit `E` strings support escaped quotes. Unterminated lexical input raises an error instead of silently changing its meaning. Dollar-quoted bodies are compared verbatim, without recursively interpreting their language. This is lexical clone detection, not semantic SQL equivalence. Thresholds remain configurable in the standard browser. + +## Roassal 3 SQL views + +The builders return unopened `RSCanvas` objects. Use any imported SQL model; there is no global `mi` dependency or implicit schema filter. For large databases pass a deliberately selected model, since all matching entities are drawn. + +```smalltalk +(FmxSQLRoassalViews schemaTablesIn: model) openWithTitle: 'Schemas and tables'. +(FmxSQLRoassalViews foreignKeysIn: model) openWithTitle: 'Foreign keys'. +(FmxSQLRoassalViews routineCallsIn: model) openWithTitle: 'Routine call candidates'. +``` + +Schema boxes contain table boxes, like class/method nesting examples. Foreign-key arrows point from the referencing table to the referenced table; composite references collapse to one edge. Call arrows point from the enclosing routine to every candidate routine, including trigger routines. Candidate edges are static-analysis evidence, not proof of runtime calls. Disconnected entities remain visible; endpoints outside the supplied model are omitted. Edges are placed above nodes, and no entities or relationships are changed. Empty models yield empty canvases. + +The historical Roassal 2/GT/Telescope sources remain in the repository for migration reference, but the unsupported `LegacyUI` load group is retired in v3. These Roassal 3 builders use current association APIs and replace the advertised visualization entry points. diff --git a/docs/sql-moose-names.md b/docs/sql-moose-names.md new file mode 100644 index 0000000..de72ac4 --- /dev/null +++ b/docs/sql-moose-names.md @@ -0,0 +1,29 @@ +# SQL Moose names for v3.0.0 + +Short `name` values remain PostgreSQL identifiers. `mooseName` is now a derived, +qualified identifier for navigation and lookup. It includes the SQL entity kind, +quoted ownership path, and input type signature for routines. For example: + +- `Table:"app"."users"` +- `Column:"app"."users"."id"` +- `Type:"app"."users"` (a distinct naming domain from the table) +- `Routine:"app"."lookup"("pg_catalog"."int4")` + +Double quotes inside identifiers are doubled. OUT/TABLE parameters do not identify +an overload; IN, INOUT and variadic parameters do. Trigger routines share the +routine naming domain. Parameter positions preserve signature order. + +Names are computed from current ownership and types, avoiding stale cached names +when a schema, owner or type is renamed or a member is reparented. `name` and +PostgreSQL OIDs are unchanged. MSE stores the original names and relations; reloading +reconstructs the same qualified names. + +This is a breaking change for scripts using `entityNamed:` with short strings or +persisting old Moose names. Pass an entity's new `mooseName`, or explicitly select +by `name` and schema when intentionally making a contextual lookup. Do not use +short labels as global identifiers. + +Unscoped entities, local AST names and synthetic not-null constraints do not claim +a globally unique Moose name. An incomplete routine signature also cannot claim +uniqueness. Duplicate catalog entities should still be diagnosed at import time; +these names do not silently merge objects. diff --git a/docs/v3-release-plan.md b/docs/v3-release-plan.md new file mode 100644 index 0000000..43ebc5b --- /dev/null +++ b/docs/v3-release-plan.md @@ -0,0 +1,55 @@ +# FAMIXNGSQL v3.0.0 qualification + +Status: review and integration candidate, not a published release. Track all work in the [v3.0.0 milestone](https://github.com/deem0n/FAMIXNGSQL/milestone/1). The [installed-image audit](moose-compatibility-audit.md) records the original failures and limits of coverage. + +## Separate fixes + +| Issue | Change | Implementation PR | +|---|---|---| +| [#11](https://github.com/deem0n/FAMIXNGSQL/issues/11) | Qualified SQL names and overloads | [SQL #26](https://github.com/deem0n/FAMIXNGSQL/pull/26) | +| [#12](https://github.com/deem0n/FAMIXNGSQL/issues/12) | Source-anchor protocol | [SQL #25](https://github.com/deem0n/FAMIXNGSQL/pull/25) | +| [#13](https://github.com/deem0n/FAMIXNGSQL/issues/13) | Scalar containment traversal | [Famix #2](https://github.com/deem0n/Famix/pull/2) | +| [#14](https://github.com/deem0n/FAMIXNGSQL/issues/14) | Standard Source Text browser | [SQL #27](https://github.com/deem0n/FAMIXNGSQL/pull/27) | +| [#15](https://github.com/deem0n/FAMIXNGSQL/issues/15) | SQL CoUsage preset | [SQL #28](https://github.com/deem0n/FAMIXNGSQL/pull/28) | +| [#16](https://github.com/deem0n/FAMIXNGSQL/issues/16) | Conservative dead-code preset | [SQL #29](https://github.com/deem0n/FAMIXNGSQL/pull/29) | +| [#17](https://github.com/deem0n/FAMIXNGSQL/issues/17) | SQL duplication cleaner | [SQL #30](https://github.com/deem0n/FAMIXNGSQL/pull/30) | +| [#18](https://github.com/deem0n/FAMIXNGSQL/issues/18) | Reject unsupported Layer Visualization inputs | [MooseIDE #4](https://github.com/deem0n/MooseIDE/pull/4) | +| [#19](https://github.com/deem0n/FAMIXNGSQL/issues/19) | Non-OO model report | [MooseIDE #2](https://github.com/deem0n/MooseIDE/pull/2) | +| [#20](https://github.com/deem0n/FAMIXNGSQL/issues/20) | Meta Browser Spec properties | [MooseIDE #1](https://github.com/deem0n/MooseIDE/pull/1) | +| [#21](https://github.com/deem0n/FAMIXNGSQL/issues/21) | Name Cloud | [Famix #1](https://github.com/deem0n/Famix/pull/1) | +| [#22](https://github.com/deem0n/FAMIXNGSQL/issues/22) | Roassal 3 SQL views | [SQL #31](https://github.com/deem0n/FAMIXNGSQL/pull/31) | +| [#23](https://github.com/deem0n/FAMIXNGSQL/issues/23) | Correct provider direction | [MooseIDE #3](https://github.com/deem0n/MooseIDE/pull/3) | +| [#24](https://github.com/deem0n/FAMIXNGSQL/issues/24) | Combined CI, immutable pins and release gate | This integration PR | + +## Dependencies and merge order + +All PRs target deem0n forks first. Famix #1/#2 plus the pre-existing namespace hierarchy and Fame fixes, and MooseIDE #1–#4 are independent review units. The draft [Famix #3](https://github.com/deem0n/Famix/pull/3) and [MooseIDE #5](https://github.com/deem0n/MooseIDE/pull/5) combine them solely for qualification; merge individual fixes first. SQL #25 and #26 can be reviewed independently. SQL #27 is stacked on #25, and #28–#31 on #27; retarget each to master after its prerequisite is merged. + +This branch combines the SQL fixes and pins Famix `a4c7ab04868fc924bb0ca470142c60d8576f6a17`. The Moose CI preparation script additionally pins complete changed MooseIDE packages at `0078da66db3575346e3a25402a90684686234f07`. These are immutable qualification commits containing the separate PRs, not moving upstream development branches. After review, ensure the same fixes are reachable from the fork default branches and refresh pins if review changes them. + +## Validation and release gates + +The CI matrix is clean Pharo 13 core/importer/generator tests and prebuilt Moose 13 with optional UI tests, both against PostgreSQL 15. CI unregisters stale Iceberg checkout registrations in the disposable prebuilt image and uses archive downloads, so absent build-machine directories cannot override pinned forks. MooseIDE packages are loaded into prebuilt Moose 13 without reloading unrelated moving baseline dependencies; the fork is also tested from clean Pharo using its full baseline. MooseIDE fork CI needs repository variable `MOOSE_PHARO_VERSIONS={"Moose-latest":["Pharo64-13"]}` because upstream organization variables are not inherited by a fork. + +Combined isolated validation: 172 passed, 2 skipped, zero failures/errors across SQL and generic-tool regression suites. The two existing Butterfly Map skips remain visible; no tests were disabled to obtain this result. Model Report HTML export emits an existing undeclared MicHTMLVisitor warning and is not qualified by the text-report fix. Individual SQL PRs #25–#31 passed both CI image jobs after correcting the prebuilt-image loading configuration. Consult each current commit's checks; this is a dated validation record, not a promise about future commits. + +- [ ] Review and merge every separate implementation PR. +- [ ] All milestone issues, including existing regeneration issue #8, meet their acceptance criteria. +- [ ] Combined clean Pharo 13 / Moose 13 / PostgreSQL 15 CI passes at the final dependency pins. +- [ ] Full metamodel regeneration and MSE tests pass without unexpected generated changes. +- [ ] Reimport all application schemas of local `mi` and repeat the installed-tool acceptance matrix at final pins; verify identities, relationships and parse-analysis reports. +- [ ] Check the supported tool paths interactively, including source highlights, selected clone display and graph navigation; record rejected/inapplicable tools honestly. +- [ ] Confirm no database artifacts or credentials are tracked. +- [ ] Publish v3.0.0 only after the above gates; do not reuse v1.0.0/v2.0.0 tags. + +The issue/PR set implements discovered incompatibilities. It does not imply universal compatibility with all Moose plugins, semantic SQL clone detection, complete parsing of dynamic SQL, or invented OO metrics/layer semantics. The installed-tool audit and final acceptance run define the evidence. + +## Breaking changes and loading + +Qualified Moose names change lookup/cache/export identifiers; see [naming migration](sql-moose-names.md). The unsupported LegacyUI load group is retired. Core stays independent of GUI packages; optional standard-tool adapters and Roassal 3 views use `MooseIDE`/`MooseIDETests` groups and require MooseIDE already installed. SQL CoUsage, dead-code and duplication use explicit SQL presets; see [working snippets](mooseide-integration.md). The generic OO defaults must not be assumed to carry SQL semantics. + +Qualification scope is Pharo 13/Moose 13/PostgreSQL 15. Do not advertise Pharo 7–12, Moose 12, Pharo 14 or other PostgreSQL versions as supported by v3 until their own clean matrix is green. Famix's broader fork matrix is separate evidence, not full-stack SQL qualification. + +The two pre-existing Famix fixes are also required: upstream [#1150](https://github.com/moosetechnology/Famix/pull/1150) (namespace hierarchy) and [#1149](https://github.com/moosetechnology/Famix/pull/1149) (Fame group inspection). Their existing branches are included in the qualification pin, with `FamixNamespaceGroupHierarchyTest` and `MooseGroupTest` required in combined CI. The original audit image already had these fixes; omitting them from a clean installation would regress it. + +Fork preservation PRs: [Famix #4](https://github.com/deem0n/Famix/pull/4) for namespace hierarchy and [Famix #5](https://github.com/deem0n/Famix/pull/5) for the already upstream-merged Fame fix. As requested in upstream #1150 review, the extra namespace workflow/configuration is removed while the seven regression tests are retained; the existing combined Moose CI runs them. diff --git a/scripts/ci/load-famix-regressions.st b/scripts/ci/load-famix-regressions.st new file mode 100644 index 0000000..feffe1f --- /dev/null +++ b/scripts/ci/load-famix-regressions.st @@ -0,0 +1,13 @@ +"Load regression classes from the same immutable Famix commit as the baseline. + The prebuilt image contains their Java/Smalltalk test model dependencies." +| repository loader | +repository := MCGitHubRepository location: + 'github://deem0n/Famix:a4c7ab04868fc924bb0ca470142c60d8576f6a17/src'. +loader := MCVersionLoader new. +#('Famix-Visualizations' 'Famix-Visualizations-Tests' 'Moose-Core-Tests' 'Moose-Query-Test') do: [ :packageName | + loader addVersion: (repository packageNamed: packageName) ]. +loader load. +#(MiMetaBrowserRegressionTest MiModelReportRegressionTest MiLayerApplicabilityTest + MiApplicationTest MiButterflyMapModelTest MiButterflyMapBrowserTest + MooseGroupNameCloudTest MQNavigationContainmentTest FamixNamespaceGroupHierarchyTest MooseGroupTest) do: [ :name | + Smalltalk at: name ifAbsent: [ self error: 'Missing required regression class: ', name ] ]. diff --git a/scripts/ci/prepare-moose.st b/scripts/ci/prepare-moose.st new file mode 100644 index 0000000..9c2806f --- /dev/null +++ b/scripts/ci/prepare-moose.st @@ -0,0 +1,17 @@ +"Use immutable archive dependencies, independent of prebuilt Iceberg checkouts. + Only affects the disposable CI image." +Iceberg enableMetacelloIntegration: false. +IceRepository registry copy do: [ :repository | + IceRepository unregisterRepository: repository ifAbsent: [ ] ]. + +"Moose 13 already supplies these tools and their dependencies. Load complete + packages from the pinned fork, avoiding unrelated moving baseline dependencies. + The MooseIDE fork itself is separately tested from clean Pharo by its full CI." +[ | repository loader | +repository := MCGitHubRepository location: + 'github://deem0n/MooseIDE:0078da66db3575346e3a25402a90684686234f07/src'. +loader := MCVersionLoader new. +#('MooseIDE-Core' 'MooseIDE-Core-Reporter' 'MooseIDE-LayerVisualization' + 'MooseIDE-Meta' 'MooseIDE-Tests') do: [ :packageName | + loader addVersion: (repository packageNamed: packageName) ]. +loader load ] value. diff --git a/src/BaselineOfFAMIXNGSQL/BaselineOfFAMIXNGSQL.class.st b/src/BaselineOfFAMIXNGSQL/BaselineOfFAMIXNGSQL.class.st index e786250..a5af2fe 100644 --- a/src/BaselineOfFAMIXNGSQL/BaselineOfFAMIXNGSQL.class.st +++ b/src/BaselineOfFAMIXNGSQL/BaselineOfFAMIXNGSQL.class.st @@ -19,6 +19,10 @@ BaselineOfFAMIXNGSQL >> baseline: spec [ requires: #('FamixNGSQL' 'PostgreSQLParser' 'PgMetadata' 'SymbolResolver' 'PetitParser') ]; package: #'FamixNGSQL-Importer-Tests' with: [ spec requires: #('FamixNGSQL-Importer' 'PgMetadata-Tests') ]; + package: #'FamixNGSQL-MooseIDE' + with: [ spec requires: #('FamixNGSQL-Importer') ]; + package: #'FamixNGSQL-MooseIDE-Tests' + with: [ spec requires: #('FamixNGSQL-MooseIDE') ]; package: #'FamixNGSQL-GTInspectorExtension' with: [ spec requires: #('FamixNGSQL-Importer') ]; package: #FAMIXNGSQLAnalysis @@ -29,7 +33,7 @@ BaselineOfFAMIXNGSQL >> baseline: spec [ spec baseline: 'Famix' with: [ spec - repository: 'github://moosetechnology/Famix:4fd41546375e0ab86a4920eff547cc898e22db0b/src'; + repository: 'github://deem0n/Famix:a4c7ab04868fc924bb0ca470142c60d8576f6a17/src'; loads: #('Core' 'Basic') ]. spec baseline: 'SymbolResolver' @@ -66,5 +70,6 @@ BaselineOfFAMIXNGSQL >> baseline: spec [ group: 'Generator' with: #('Core' 'FAMIXNGSQLMetamodelGenerator'); group: 'GeneratorTests' with: #('Generator' 'FAMIXNGSQLMetamodelGenerator-Tests'); group: 'Tests' with: #('Core' 'FamixNGSQL-Importer-Tests'); - group: 'LegacyUI' with: #('Core' 'FamixNGSQL-GTInspectorExtension' 'FAMIXNGSQLAnalysis' 'FAMIXNGSQLAnalysis-ConnectionsManager') ] + group: 'MooseIDE' with: #('Core' 'FamixNGSQL-MooseIDE'); + group: 'MooseIDETests' with: #('MooseIDE' 'FamixNGSQL-MooseIDE-Tests') ] ] diff --git a/src/FAMIXNGSQLMetamodelGenerator-Tests/FmxSQLNamingRegenerationTest.class.st b/src/FAMIXNGSQLMetamodelGenerator-Tests/FmxSQLNamingRegenerationTest.class.st new file mode 100644 index 0000000..63b9e23 --- /dev/null +++ b/src/FAMIXNGSQLMetamodelGenerator-Tests/FmxSQLNamingRegenerationTest.class.st @@ -0,0 +1,15 @@ +Class { + #name : #FmxSQLNamingRegenerationTest, + #superclass : #TestCase, + #category : #'FAMIXNGSQLMetamodelGenerator-Tests' +} +{ #category : #tests } +FmxSQLNamingRegenerationTest >> testNamesSurviveGeneration [ + | before after | + before := (MCPackage named: 'FamixNGSQL') snapshot. + FmxSQLMetamodelGenerator generate. + after := (MCPackage named: 'FamixNGSQL') snapshot. + self assertEmpty: (after patchRelativeToBase: before) operations. + self assert: ((FmxSQLNamedEntity >> #mooseName) package name) equals: 'FamixNGSQL-Importer'. + self assert: (FmxSQLNamespace new name: 'test'; yourself) mooseName equals: 'Namespace:"test"' +] diff --git a/src/FamixNGSQL-Importer-Tests/FmxSQLMooseNameTest.class.st b/src/FamixNGSQL-Importer-Tests/FmxSQLMooseNameTest.class.st new file mode 100644 index 0000000..04d28ad --- /dev/null +++ b/src/FamixNGSQL-Importer-Tests/FmxSQLMooseNameTest.class.st @@ -0,0 +1,72 @@ +Class { + #name : #FmxSQLMooseNameTest, + #superclass : #TestCase, + #category : #'FamixNGSQL-Importer-Tests' +} +{ #category : #tests } +FmxSQLMooseNameTest >> testSchemasAndLookup [ + | model a b one two | + model := FmxSQLModel new. + one := FmxSQLNamespace new name: 'one'; mooseModel: model; yourself. + two := FmxSQLNamespace new name: 'two'; mooseModel: model; yourself. + a := FmxSQLTable new name: 'same'; parentNamespace: one; mooseModel: model; yourself. + b := FmxSQLTable new name: 'same'; parentNamespace: two; mooseModel: model; yourself. + self assert: a mooseName equals: 'Table:"one"."same"'. + self deny: a mooseName equals: b mooseName. + self assert: (model entityNamed: a mooseName) identicalTo: a. + self assert: (model entityNamed: b mooseName) identicalTo: b +] +{ #category : #tests } +FmxSQLMooseNameTest >> testOwnersQuotingAndChanges [ + | namespace table column | + namespace := FmxSQLNamespace new name: 'a.b'; yourself. + table := FmxSQLTable new name: 'a"b'; parentNamespace: namespace; yourself. + column := FmxSQLColumn new name: 'id'; columnsContainer: table; yourself. + self assert: column mooseName equals: 'Column:"a.b"."a""b"."id"'. + namespace name: 'new'. + self assert: column mooseName equals: 'Column:"new"."a""b"."id"'. + table parentNamespace: (FmxSQLNamespace new name: 'other'; yourself). + self assert: column mooseName equals: 'Column:"other"."a""b"."id"'. + self assert: column name equals: 'id' +] +{ #category : #tests } +FmxSQLMooseNameTest >> testTableAndTypeNamingDomains [ + | namespace table type | + namespace := FmxSQLNamespace new name: 'public'; yourself. + table := FmxSQLTable new name: 'record'; parentNamespace: namespace; yourself. + type := FmxSQLType new name: 'record'; parentNamespace: namespace; yourself. + self assert: table hasUniqueMooseNameInModel; assert: type hasUniqueMooseNameInModel. + self deny: table mooseName equals: type mooseName +] +{ #category : #tests } +FmxSQLMooseNameTest >> testOverloadsExcludeOutputParameters [ + | namespace integer text routine other | + namespace := FmxSQLNamespace new name: 'public'; yourself. + integer := FmxSQLType new name: 'int4'; parentNamespace: namespace; yourself. + text := FmxSQLType new name: 'text'; parentNamespace: namespace; yourself. + routine := FmxSQLStoredProcedure new name: 'f'; parentNamespace: namespace; yourself. + other := FmxSQLStoredProcedure new name: 'f'; parentNamespace: namespace; yourself. + FmxSQLParameter new name: 'input'; position: 1; mode: 'i'; type: integer; parentBehaviouralEntity: routine. + FmxSQLParameter new name: 'output'; position: 2; mode: 'o'; type: text; parentBehaviouralEntity: routine. + FmxSQLParameter new name: 'input'; position: 1; mode: 'b'; type: text; parentBehaviouralEntity: other. + self assert: routine mooseName equals: 'Routine:"public"."f"("public"."int4")'. + self deny: routine mooseName equals: other mooseName. + self assert: routine hasUniqueMooseNameInModel +] +{ #category : #tests } +FmxSQLMooseNameTest >> testUnscopedAndLocalNamesDoNotClaimUniqueness [ + self deny: (FmxSQLTable new name: 'unscoped'; yourself) hasUniqueMooseNameInModel. + self deny: (FmxSQLLocalVariable new name: 'repeated'; yourself) hasUniqueMooseNameInModel +] +{ #category : #tests } +FmxSQLMooseNameTest >> testRoundTrip [ + | model namespace table restored name | + model := FmxSQLModel new. + namespace := FmxSQLNamespace new name: 'example'; mooseModel: model; yourself. + table := FmxSQLTable new name: 'items'; parentNamespace: namespace; mooseModel: model; yourself. + name := table mooseName. + restored := FmxSQLModel importFromMSEStream: + (String streamContents: [ :stream | model exportToMSEStream: stream ]) readStream. + self assert: (restored entityNamed: name) class equals: FmxSQLTable. + self assert: (restored entityNamed: name) name equals: 'items' +] diff --git a/src/FamixNGSQL-Importer-Tests/FmxSQLSourceAnchorProtocolTest.class.st b/src/FamixNGSQL-Importer-Tests/FmxSQLSourceAnchorProtocolTest.class.st new file mode 100644 index 0000000..b29ff16 --- /dev/null +++ b/src/FamixNGSQL-Importer-Tests/FmxSQLSourceAnchorProtocolTest.class.st @@ -0,0 +1,58 @@ +Class { + #name : #FmxSQLSourceAnchorProtocolTest, + #superclass : #TestCase, + #category : #'FamixNGSQL-Importer-Tests' +} + +{ #category : #tests } +FmxSQLSourceAnchorProtocolTest >> testValidInterval [ + | anchor | + anchor := FmxSQLEntitySourceAnchor new + entity: (FmxSQLStoredProcedure new source: 'SELECT 42'); start: 8; end: 9; yourself. + self assert: anchor knowsStart. + self assert: anchor knowsEnd. + self assert: anchor hasSource. + self assert: anchor sourceText equals: '42' +] + +{ #category : #tests } +FmxSQLSourceAnchorProtocolTest >> testMissingSource [ + | anchor | + anchor := FmxSQLEntitySourceAnchor new start: 1; end: 1; yourself. + self deny: anchor knowsStart; deny: anchor knowsEnd; deny: anchor hasSource. + anchor entity: FmxSQLStoredProcedure new. + self deny: anchor knowsStart; deny: anchor knowsEnd; deny: anchor hasSource. + anchor entity source: ''. + self deny: anchor knowsStart; deny: anchor knowsEnd; deny: anchor hasSource +] + +{ #category : #tests } +FmxSQLSourceAnchorProtocolTest >> testPartialAndInvalidIntervals [ + | anchor | + anchor := FmxSQLEntitySourceAnchor new entity: (FmxSQLStoredProcedure new source: 'SELECT 42'); yourself. + anchor start: 1. + self assert: anchor knowsStart; deny: anchor knowsEnd; deny: anchor hasSource. + anchor start: nil; end: 9. + self deny: anchor knowsStart; assert: anchor knowsEnd; deny: anchor hasSource. + #(0 -1 10 1.5) do: [ :offset | + anchor start: offset; end: offset. + self deny: anchor knowsStart; deny: anchor knowsEnd; deny: anchor hasSource ]. + anchor start: 8; end: 2. + self assert: anchor knowsStart; deny: anchor knowsEnd; deny: anchor hasSource. + self assert: anchor sourceText equals: '' +] + +{ #category : #tests } +FmxSQLSourceAnchorProtocolTest >> testProtocolSurvivesRoundTrip [ + | model owner query anchor restored | + model := FmxSQLModel new. + owner := FmxSQLStoredProcedure new name: 'example'; source: 'SELECT 42'; mooseModel: model; yourself. + query := FmxSQLSelectQuery new container: owner; mooseModel: model; yourself. + anchor := FmxSQLEntitySourceAnchor new entity: owner; start: 8; end: 9; mooseModel: model; yourself. + query sourceAnchor: anchor. + restored := FmxSQLModel importFromMSEStream: + (String streamContents: [ :stream | model exportToMSEStream: stream ]) readStream. + anchor := (restored allWithType: FmxSQLEntitySourceAnchor) anyOne. + self assert: anchor knowsStart; assert: anchor knowsEnd; assert: anchor hasSource. + self assert: anchor sourceText equals: '42' +] diff --git a/src/FamixNGSQL-Importer-Tests/FmxSQLV3NavigationAcceptanceTest.class.st b/src/FamixNGSQL-Importer-Tests/FmxSQLV3NavigationAcceptanceTest.class.st new file mode 100644 index 0000000..99583ec --- /dev/null +++ b/src/FamixNGSQL-Importer-Tests/FmxSQLV3NavigationAcceptanceTest.class.st @@ -0,0 +1,20 @@ +Class { + #name : #FmxSQLV3NavigationAcceptanceTest, + #superclass : #TestCase, + #category : #'FamixNGSQL-Importer-Tests' +} +{ #category : #tests } +FmxSQLV3NavigationAcceptanceTest >> testViewDependencyTraversal [ + | model view query clause table reference | + model := FmxSQLModel new. + view := FmxSQLView new name: 'example'; mooseModel: model; yourself. + query := FmxSQLSelectQuery new viewDefining: view; mooseModel: model; yourself. + clause := FmxSQLFromClause new sqlQuery: query; mooseModel: model; yourself. + table := FmxSQLTable new name: 'records'; mooseModel: model; yourself. + reference := FmxSQLTableReference new source: clause; table: table; mooseModel: model; yourself. + self assert: (view queryAllOutgoing includes: reference). + self assert: (view allProviders includes: table). + self assert: view hasOutgoing. + self deny: view hasLocalOutgoing. + self assert: (table allClients includes: clause) +] diff --git a/src/FamixNGSQL-Importer/FmxSQLCursorParameter.extension.st b/src/FamixNGSQL-Importer/FmxSQLCursorParameter.extension.st index 43dd7ff..5527047 100644 --- a/src/FamixNGSQL-Importer/FmxSQLCursorParameter.extension.st +++ b/src/FamixNGSQL-Importer/FmxSQLCursorParameter.extension.st @@ -8,3 +8,13 @@ FmxSQLCursorParameter >> createReferenceEntityFrom: aFmxSQLEntity [ mooseModel: aFmxSQLEntity mooseModel; yourself. ] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLCursorParameter >> mooseNameOn: aStream [ + aStream nextPutAll: (FmxSQLMooseName nameFor: self) +] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLCursorParameter >> hasUniqueMooseNameInModel [ + ^ FmxSQLMooseName isUniqueFor: self +] diff --git a/src/FamixNGSQL-Importer/FmxSQLEntitySourceAnchor.extension.st b/src/FamixNGSQL-Importer/FmxSQLEntitySourceAnchor.extension.st index dd63d96..01de305 100644 --- a/src/FamixNGSQL-Importer/FmxSQLEntitySourceAnchor.extension.st +++ b/src/FamixNGSQL-Importer/FmxSQLEntitySourceAnchor.extension.st @@ -14,3 +14,21 @@ FmxSQLEntitySourceAnchor >> sourceText [ ifTrue: [ self entity source copyFrom: self start to: self end ] ifFalse: [ '' ] ] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLEntitySourceAnchor >> hasSource [ + ^ self hasSourceText +] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLEntitySourceAnchor >> knowsStart [ + ^ self entity notNil and: [ self entity source notNil and: [ + self start isInteger and: [ self start > 0 and: [ self start <= self entity source size ] ] ] ] +] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLEntitySourceAnchor >> knowsEnd [ + ^ self entity notNil and: [ self entity source notNil and: [ + self end isInteger and: [ self end > 0 and: [ self end <= self entity source size and: [ + self start isNil or: [ self start isInteger and: [ self end >= self start ] ] ] ] ] ] ] +] diff --git a/src/FamixNGSQL-Importer/FmxSQLLocalVariable.extension.st b/src/FamixNGSQL-Importer/FmxSQLLocalVariable.extension.st index 7847fd8..36e77e6 100644 --- a/src/FamixNGSQL-Importer/FmxSQLLocalVariable.extension.st +++ b/src/FamixNGSQL-Importer/FmxSQLLocalVariable.extension.st @@ -16,3 +16,13 @@ FmxSQLLocalVariable >> createReferenceEntityFrom: aFmxSQLEntity [ FmxSQLLocalVariable >> reachableEntities [ ^ self type ifNil: [ #() ] ifNotNil: [ :aType | aType reachableEntities ] ] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLLocalVariable >> mooseNameOn: aStream [ + aStream nextPutAll: (FmxSQLMooseName nameFor: self) +] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLLocalVariable >> hasUniqueMooseNameInModel [ + ^ FmxSQLMooseName isUniqueFor: self +] diff --git a/src/FamixNGSQL-Importer/FmxSQLMooseName.class.st b/src/FamixNGSQL-Importer/FmxSQLMooseName.class.st new file mode 100644 index 0000000..1a1c31e --- /dev/null +++ b/src/FamixNGSQL-Importer/FmxSQLMooseName.class.st @@ -0,0 +1,69 @@ +"Canonical SQL catalog names. Display names remain untouched. +Non-catalog AST names are readable but do not claim global uniqueness." +Class { + #name : #FmxSQLMooseName, + #superclass : #Object, + #category : #'FamixNGSQL-Importer' +} +{ #category : #naming } +FmxSQLMooseName class >> quoted: aName [ + ^ '"', ((aName ifNil: [ '' ]) asString copyReplaceAll: '"' with: '""'), '"' +] +{ #category : #naming } +FmxSQLMooseName class >> ownerOf: entity [ + #(columnsContainer table parentBehaviouralEntity parentNamespace) do: [ :selector | + (entity respondsTo: selector) ifTrue: [ + (entity perform: selector) ifNotNil: [ :owner | ^ owner ] ] ]. + ^ nil +] +{ #category : #naming } +FmxSQLMooseName class >> pathFor: entity [ + ^ self pathFor: entity visited: IdentitySet new +] +{ #category : #naming } +FmxSQLMooseName class >> pathFor: entity visited: visited [ + | owner prefix result | + (visited includes: entity) ifTrue: [ ^ '' ]. + visited add: entity. + owner := self ownerOf: entity. + prefix := owner ifNil: [ '' ] ifNotNil: [ (self pathFor: owner visited: visited), '.' ]. + result := prefix, (self quoted: entity name). + (entity isKindOf: FmxSQLStoredProcedure) ifTrue: [ + result := result, '(', (((self inputParametersOf: entity) collect: [ :parameter | + parameter type ifNil: [ '?' ] ifNotNil: [ :type | self pathFor: type ] ]) joinUsing: ','), ')' ]. + ^ result +] +{ #category : #naming } +FmxSQLMooseName class >> inputParametersOf: routine [ + ^ (routine parameters reject: [ :parameter | #('o' 't' 'OUT' 'TABLE') includes: parameter mode ]) + sorted: [ :a :b | (a position ifNil: [ 0 ]) < (b position ifNil: [ 0 ]) ] +] +{ #category : #naming } +FmxSQLMooseName class >> nameFor: entity [ + "The entity kind separates PostgreSQL's independent naming domains, e.g. a table and its row type." + | kind | + kind := (entity isKindOf: FmxSQLStoredProcedure) + ifTrue: [ 'Routine' ] ifFalse: [ entity class name allButFirst: 'FmxSQL' size ]. + ^ kind, ':', (self pathFor: entity) +] +{ #category : #testing } +FmxSQLMooseName class >> isUniqueFor: entity [ + ^ self isUniqueFor: entity visited: IdentitySet new +] +{ #category : #testing } +FmxSQLMooseName class >> isUniqueFor: entity visited: visited [ + | owner catalogEntity | + (visited includes: entity) ifTrue: [ ^ false ]. + visited add: entity. + (entity name isNil or: [ entity name isEmpty ]) ifTrue: [ ^ false ]. + (entity isKindOf: FmxSQLNamespace) ifTrue: [ ^ true ]. + catalogEntity := { FmxSQLTable. FmxSQLForeignTable. FmxSQLView. FmxSQLType. + FmxSQLColumn. FmxSQLTrigger. FmxSQLStoredProcedure. FmxSQLConstraint } + anySatisfy: [ :class | entity isKindOf: class ]. + (catalogEntity not or: [ entity isKindOf: FmxSQLNotNullConstraint ]) ifTrue: [ ^ false ]. + (entity isKindOf: FmxSQLStoredProcedure) ifTrue: [ + ((self inputParametersOf: entity) allSatisfy: [ :p | + p position isInteger and: [ p type notNil and: [ self isUniqueFor: p type ] ] ]) ifFalse: [ ^ false ] ]. + owner := self ownerOf: entity. + ^ owner notNil and: [ self isUniqueFor: owner visited: visited ] +] diff --git a/src/FamixNGSQL-Importer/FmxSQLNamedEntity.extension.st b/src/FamixNGSQL-Importer/FmxSQLNamedEntity.extension.st new file mode 100644 index 0000000..648da09 --- /dev/null +++ b/src/FamixNGSQL-Importer/FmxSQLNamedEntity.extension.st @@ -0,0 +1,16 @@ +Extension { #name : #FmxSQLNamedEntity } + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLNamedEntity >> mooseName [ + ^ FmxSQLMooseName nameFor: self +] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLNamedEntity >> mooseNameOn: aStream [ + aStream nextPutAll: (FmxSQLMooseName nameFor: self) +] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLNamedEntity >> hasUniqueMooseNameInModel [ + ^ FmxSQLMooseName isUniqueFor: self +] diff --git a/src/FamixNGSQL-Importer/FmxSQLNamespace.extension.st b/src/FamixNGSQL-Importer/FmxSQLNamespace.extension.st index 2bb8099..bca9be7 100644 --- a/src/FamixNGSQL-Importer/FmxSQLNamespace.extension.st +++ b/src/FamixNGSQL-Importer/FmxSQLNamespace.extension.st @@ -13,3 +13,18 @@ FmxSQLNamespace >> createReferenceEntityFrom: aFmxSQLEntity [ FmxSQLNamespace >> reachableEntities [ ^ self namespaceEntities ] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLNamespace >> mooseName [ + ^ FmxSQLMooseName nameFor: self +] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLNamespace >> mooseNameOn: aStream [ + aStream nextPutAll: (FmxSQLMooseName nameFor: self) +] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLNamespace >> hasUniqueMooseNameInModel [ + ^ FmxSQLMooseName isUniqueFor: self +] diff --git a/src/FamixNGSQL-Importer/FmxSQLParameter.extension.st b/src/FamixNGSQL-Importer/FmxSQLParameter.extension.st index 3ade897..1c10b0b 100644 --- a/src/FamixNGSQL-Importer/FmxSQLParameter.extension.st +++ b/src/FamixNGSQL-Importer/FmxSQLParameter.extension.st @@ -13,3 +13,13 @@ FmxSQLParameter >> createReferenceEntityFrom: aFmxSQLEntity [ mooseModel: aFmxSQLEntity mooseModel; yourself ] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLParameter >> mooseNameOn: aStream [ + aStream nextPutAll: (FmxSQLMooseName nameFor: self) +] + +{ #category : #'*FamixNGSQL-Importer' } +FmxSQLParameter >> hasUniqueMooseNameInModel [ + ^ FmxSQLMooseName isUniqueFor: self +] diff --git a/src/FamixNGSQL-MooseIDE-Tests/FmxSQLCoUsageTest.class.st b/src/FamixNGSQL-MooseIDE-Tests/FmxSQLCoUsageTest.class.st new file mode 100644 index 0000000..09ecbdd --- /dev/null +++ b/src/FamixNGSQL-MooseIDE-Tests/FmxSQLCoUsageTest.class.st @@ -0,0 +1,41 @@ +Class { + #name : #FmxSQLCoUsageTest, + #superclass : #TestCase, + #category : #'FamixNGSQL-MooseIDE-Tests' +} +{ #category : #tests } +FmxSQLCoUsageTest >> testSharedRelationsAndRepeatedReferences [ + | model table column first second extractor browser | + model := FmxSQLModel new. + table := FmxSQLTable new name: 'records'; mooseModel: model; yourself. + column := FmxSQLColumn new name: 'id'; columnsContainer: table; mooseModel: model; yourself. + first := FmxSQLStoredProcedure new name: 'first'; mooseModel: model; yourself. + second := FmxSQLStoredProcedure new name: 'second'; mooseModel: model; yourself. + FmxSQLColumnReference new source: first; column: column; mooseModel: model. + FmxSQLTableReference new source: first; table: table; mooseModel: model. + FmxSQLTableReference new source: second; table: table; mooseModel: model. + extractor := FmxSQLCoUsageExtractor new. + self assert: (extractor innerEntitiesFor: first) asArray equals: { table. table }. + self assert: (extractor innerEntitiesFor: second) asArray equals: { table }. + browser := FmxSQLCoUsageExtractor openOn: { first. second }. + [ self assert: browser specModel containerBoxes size equals: 2. + self assert: browser specModel innerBoxes size equals: 1 ] + ensure: [ browser window close ] +] +{ #category : #tests } +FmxSQLCoUsageTest >> testForeignKeyAndEmptyTable [ + | model source target column constraint | + model := FmxSQLModel new. + source := FmxSQLTable new name: 'source'; mooseModel: model; yourself. + target := FmxSQLTable new name: 'target'; mooseModel: model; yourself. + column := FmxSQLColumn new name: 'id'; columnsContainer: target; mooseModel: model; yourself. + constraint := FmxSQLForeignKeyConstraint new name: 'fk'; table: source; mooseModel: model; yourself. + FmxSQLColumnReference new foreignKeyConstraintSource: constraint; column: column; mooseModel: model. + self assert: (FmxSQLCoUsageExtractor new innerEntitiesFor: source) asArray equals: { target }. + self assertEmpty: (FmxSQLCoUsageExtractor new innerEntitiesFor: target) +] +{ #category : #tests } +FmxSQLCoUsageTest >> testUnsupportedInputIsRejectedBeforeOpening [ + self should: [ FmxSQLCoUsageExtractor openOn: { Object new } ] raise: Error. + self should: [ FmxSQLCoUsageExtractor new innerEntitiesFor: Object new ] raise: Error +] diff --git a/src/FamixNGSQL-MooseIDE-Tests/FmxSQLDeadCodeTest.class.st b/src/FamixNGSQL-MooseIDE-Tests/FmxSQLDeadCodeTest.class.st new file mode 100644 index 0000000..f79362c --- /dev/null +++ b/src/FamixNGSQL-MooseIDE-Tests/FmxSQLDeadCodeTest.class.st @@ -0,0 +1,54 @@ +Class { + #name : #FmxSQLDeadCodeTest, + #superclass : #TestCase, + #category : #'FamixNGSQL-MooseIDE-Tests' +} +{ #category : #tests } +FmxSQLDeadCodeTest >> testDefaultProtectsExternalAndUncertainRoutines [ + | routine rule | + routine := FmxSQLStoredProcedure new. + rule := FmxSQLDeadCodeRule new. + self deny: rule selectedByDefault. + self assert: (rule notDead: routine). + rule closedWorldCandidates: { routine }. + self assert: (rule notDead: routine). + routine cacheAt: #analysisStatus put: #visited. + self deny: (rule notDead: routine). + rule externalEntryPoints: { routine }. + self assert: (rule notDead: routine). + self deny: (rule notDead: FamixStMethod new) +] +{ #category : #tests } +FmxSQLDeadCodeTest >> testTriggerBindingProtectsClosedWorldCandidate [ + | routine rule | + routine := FmxSQLTriggerStoredProcedure new. + routine cacheAt: #analysisStatus put: #visited. + FmxSQLTrigger new storedProcedure: routine. + rule := FmxSQLDeadCodeRule new closedWorldCandidates: { routine }; yourself. + self assert: (rule notDead: routine) +] +{ #category : #tests } +FmxSQLDeadCodeTest >> testConstraintBindingProtectsClosedWorldCandidate [ + | routine rule | + routine := FmxSQLStoredProcedure new. + routine cacheAt: #analysisStatus put: #visited. + FmxSQLCheckConstraint new addStoredProceduresCalled: routine. + rule := FmxSQLDeadCodeRule new closedWorldCandidates: { routine }; yourself. + self assert: (rule notDead: routine) +] +{ #category : #tests } +FmxSQLDeadCodeTest >> testStandardBrowserUsesConservativeDefault [ + | model routine browser rule | + model := FmxSQLModel new. + routine := FmxSQLStoredProcedure new name: 'candidate'; mooseModel: model; yourself. + routine cacheAt: #analysisStatus put: #visited. + browser := FmxSQLDeadCodeRule openOn: { routine }. + [ + browser specModel runComputation. + self assertEmpty: browser specModel deadEntities. + rule := browser specModel chosenRules detect: [ :each | each isKindOf: FmxSQLDeadCodeRule ]. + rule closedWorldCandidates: { routine }. + browser specModel runComputation. + self assert: (browser specModel deadEntities includes: routine) ] + ensure: [ browser window ifNotNil: #close ] +] diff --git a/src/FamixNGSQL-MooseIDE-Tests/FmxSQLReplicationCleanerTest.class.st b/src/FamixNGSQL-MooseIDE-Tests/FmxSQLReplicationCleanerTest.class.st new file mode 100644 index 0000000..06702cd --- /dev/null +++ b/src/FamixNGSQL-MooseIDE-Tests/FmxSQLReplicationCleanerTest.class.st @@ -0,0 +1,60 @@ +Class { + #name : #FmxSQLReplicationCleanerTest, + #superclass : #TestCase, + #category : #'FamixNGSQL-MooseIDE-Tests' +} +{ #category : #tests } +FmxSQLReplicationCleanerTest >> testCommentsAndTokenBoundaries [ + self assert: (FmxSQLReplicationCleaner new cleanedTextLines: + '-- heading', String lf, 'SELECT/* a /* nested */ b */x -- end') asArray + equals: { 2 -> 'SELECT x' } +] +{ #category : #tests } +FmxSQLReplicationCleanerTest >> testQuotesAndDollarBodiesAreVerbatim [ + | text | + text := 'SELECT ''https://x -- /* y */'', "a""--b", $tag$ /* -- */ $tag$, ''it''''s ok'''. + self assert: (FmxSQLReplicationCleaner new cleanedTextLines: text) first value equals: text. + self deny: (FmxSQLReplicationCleaner new cleanedTextLines: 'SELECT ''a b''') + equals: (FmxSQLReplicationCleaner new cleanedTextLines: 'SELECT ''ab''') +] +{ #category : #tests } +FmxSQLReplicationCleanerTest >> testEscapedQuoteAndMultilineLiteral [ + | text lines | + text := 'SELECT E''a\''-- b'', $$', String lf, ' -- body ', String lf, '$$;', String lf, + '/* comment', String lf, '*/', String lf, 'SELECT 2'. + lines := FmxSQLReplicationCleaner new cleanedTextLines: text. + self assert: lines second equals: 2 -> ' -- body '. + self assert: lines last equals: 6 -> 'SELECT 2'. + self assert: lines first value equals: 'SELECT E''a\''-- b'', $$' +] +{ #category : #tests } +FmxSQLReplicationCleanerTest >> testRejectsUnterminatedLexemes [ + { '/* missing'. 'SELECT ''missing'. 'SELECT $a$missing' } do: [ :text | + self should: [ FmxSQLReplicationCleaner new cleanedTextLines: text ] raise: Error ] +] +{ #category : #tests } +FmxSQLReplicationCleanerTest >> testStandardDetectorOnSQLSourceAnchors [ + | entities configuration manager | + entities := (1 to: 2) collect: [ :i | | owner query | + owner := FmxSQLView new name: 'view', i asString; source: 'SELECT 1', String lf, 'FROM items'; yourself. + query := FmxSQLSelectQuery new. + query sourceAnchor: (FmxSQLEntitySourceAnchor new entity: owner; start: 1; end: owner source size; yourself). + query ]. + configuration := FamixRepConfiguration sourcesCleaner: FmxSQLReplicationCleaner new + minimumNumberOfReplicas: 2 ofLines: 2 ofCharacters: 1. + manager := FamixRepDetector new configuration: configuration; runOn: entities. + self assert: manager replicatedFragments notEmpty. + self assert: manager replicatedFragments anyOne replicas anyOne codeText equals: entities first sourceText +] + +{ #category : #tests } +FmxSQLReplicationCleanerTest >> testStandardBrowserUsesSQLCleaner [ + | owner query browser | + owner := FmxSQLView new source: 'SELECT 1'; yourself. + query := FmxSQLSelectQuery new. + query sourceAnchor: (FmxSQLEntitySourceAnchor new entity: owner; start: 1; end: 8; yourself). + browser := FmxSQLReplicationCleaner openOn: { query }. + [ self assert: browser specModel currentConfiguration sourcesCleaner class equals: FmxSQLReplicationCleaner. + self assert: (MiDuplicationBrowserModel availableSourcesCleaners anySatisfy: [ :cleaner | + cleaner class = FmxSQLReplicationCleaner ]) ] ensure: [ browser window close ] +] diff --git a/src/FamixNGSQL-MooseIDE-Tests/FmxSQLRoassalViewsTest.class.st b/src/FamixNGSQL-MooseIDE-Tests/FmxSQLRoassalViewsTest.class.st new file mode 100644 index 0000000..63b0f35 --- /dev/null +++ b/src/FamixNGSQL-MooseIDE-Tests/FmxSQLRoassalViewsTest.class.st @@ -0,0 +1,60 @@ +Class { + #name : #FmxSQLRoassalViewsTest, + #superclass : #TestCase, + #category : #'FamixNGSQL-MooseIDE-Tests' +} +{ #category : #tests } +FmxSQLRoassalViewsTest >> testEmptyModel [ + | model | + model := FmxSQLModel new. + #(schemaTablesIn: foreignKeysIn: routineCallsIn:) do: [ :selector | + | canvas | + canvas := FmxSQLRoassalViews perform: selector with: model. + self assert: canvas nodes isEmpty. + self assert: canvas edges isEmpty ] +] +{ #category : #tests } +FmxSQLRoassalViewsTest >> testCompositeForeignKeysAndDisconnectedTable [ + | model source target lonely foreignKey canvas before | + model := FmxSQLModel new. + source := FmxSQLTable new name: 'orders'; mooseModel: model; yourself. + target := FmxSQLTable new name: 'customers'; mooseModel: model; yourself. + lonely := FmxSQLTable new name: 'disconnected'; mooseModel: model; yourself. + foreignKey := FmxSQLForeignKeyConstraint new table: source; mooseModel: model; yourself. + 2 timesRepeat: [ | column | + column := FmxSQLColumn new columnsContainer: target; mooseModel: model; yourself. + FmxSQLColumnReference new foreignKeyConstraintSource: foreignKey; target: column; mooseModel: model ]. + before := model entities size. + self assert: (FmxSQLRoassalViews foreignKeyPairsIn: model) asArray equals: { { source. target } }. + canvas := FmxSQLRoassalViews foreignKeysIn: model. + self assert: canvas nodes size equals: 3. + self assert: canvas edges size equals: 1. + self assert: canvas edges first from model identicalTo: source. + self assert: canvas edges first to model identicalTo: target. + self assert: model entities size equals: before +] +{ #category : #tests } +FmxSQLRoassalViewsTest >> testCallCandidatesIncludingTriggerAndExternalTarget [ + | model caller target trigger external canvas | + model := FmxSQLModel new. + caller := FmxSQLStoredProcedure new name: 'caller'; mooseModel: model; yourself. + target := FmxSQLStoredProcedure new name: 'callee'; mooseModel: model; yourself. + trigger := FmxSQLTriggerStoredProcedure new name: 'trigger'; mooseModel: model; yourself. + external := FmxSQLStoredProcedure new name: 'outside'; yourself. + FmxSQLStoredProcedureCall new sender: caller; candidates: { target. trigger. external }; mooseModel: model. + self assert: (FmxSQLRoassalViews routineCallPairsIn: model) size equals: 2. + canvas := FmxSQLRoassalViews routineCallsIn: model. + self assert: canvas nodes size equals: 3. + self assert: canvas edges size equals: 2 +] +{ #category : #tests } +FmxSQLRoassalViewsTest >> testSchemaContainsTableShapes [ + | model schema table canvas | + model := FmxSQLModel new. + schema := FmxSQLNamespace new name: 'app'; mooseModel: model; yourself. + table := FmxSQLTable new name: 'items'; parentNamespace: schema; mooseModel: model; yourself. + canvas := FmxSQLRoassalViews schemaTablesIn: model. + self assert: canvas nodes size equals: 1. + self assert: canvas nodes first model identicalTo: schema. + self assert: (canvas nodes first allChildren anySatisfy: [ :shape | shape model == table ]) +] diff --git a/src/FamixNGSQL-MooseIDE-Tests/FmxSQLSourceTextAdapterTest.class.st b/src/FamixNGSQL-MooseIDE-Tests/FmxSQLSourceTextAdapterTest.class.st new file mode 100644 index 0000000..74e96b4 --- /dev/null +++ b/src/FamixNGSQL-MooseIDE-Tests/FmxSQLSourceTextAdapterTest.class.st @@ -0,0 +1,54 @@ +Class { + #name : #FmxSQLSourceTextAdapterTest, + #superclass : #TestCase, + #category : #'FamixNGSQL-MooseIDE-Tests' +} +{ #category : #tests } +FmxSQLSourceTextAdapterTest >> testAdapterSelectionAndPlainSourceFallback [ + | routine adapter | + routine := FmxSQLStoredProcedure new source: 'SELECT 1'; yourself. + adapter := MiAbstractSourceTextAnchorAdapter adapterFor: routine. + self assert: adapter class equals: FmxSQLSourceTextAdapter. + adapter initializeDisplayedText: routine sourceText andEntity: routine. + self assert: (adapter intervalForEntity: routine) isNil. + self assert: routine sourceText equals: 'SELECT 1' +] +{ #category : #tests } +FmxSQLSourceTextAdapterTest >> testFragmentOffsetsAndForeignOwner [ + | owner query reference adapter | + owner := FmxSQLStoredProcedure new source: 'prefix SELECT x suffix'; yourself. + query := FmxSQLSelectQuery new sourceAnchor: + (FmxSQLEntitySourceAnchor new entity: owner; start: 8; end: 15; yourself); yourself. + reference := FmxSQLColumnReference new sourceAnchor: + (FmxSQLEntitySourceAnchor new entity: owner; start: 15; end: 15; yourself); yourself. + adapter := FmxSQLSourceTextAdapter new initializeDisplayedText: query sourceText andEntity: query; yourself. + self assert: (adapter intervalForEntity: reference) equals: (8 to: 8). + reference sourceAnchor start: 1; end: 6. + self assert: (adapter intervalForEntity: reference) isNil. + reference sourceAnchor entity: (FmxSQLStoredProcedure new source: owner source; yourself); start: 15; end: 15. + self assert: (adapter intervalForEntity: reference) isNil +] +{ #category : #tests } +FmxSQLSourceTextAdapterTest >> testInvalidAnchorHasNoHighlight [ + | routine reference adapter | + routine := FmxSQLStoredProcedure new source: 'SELECT 1'; yourself. + reference := FmxSQLColumnReference new sourceAnchor: + (FmxSQLEntitySourceAnchor new entity: routine; start: 0; end: 99; yourself); yourself. + adapter := FmxSQLSourceTextAdapter new initializeDisplayedText: routine sourceText andEntity: routine; yourself. + self assert: (adapter intervalForEntity: reference) isNil +] +{ #category : #tests } +FmxSQLSourceTextAdapterTest >> testStandardBrowserOnRoutineAndView [ + | model view routine browser | + model := FmxSQLModel new. + view := FmxSQLView new name: 'example'; source: 'SELECT 1'; mooseModel: model; yourself. + FmxSQLSelectQuery new viewDefining: view; mooseModel: model. + routine := FmxSQLStoredProcedure new name: 'example'; source: 'SELECT 1'; mooseModel: model; yourself. + browser := MiSourceTextBrowser new. + [ browser open. + { routine. view } do: [ :entity | + browser followEntity: entity. + self assert: browser specModel selectedEntity identicalTo: entity. + self assert: browser specModel rendererModel displayedText asString equals: entity sourceText ] ] + ensure: [ browser window ifNotNil: #close ] +] diff --git a/src/FamixNGSQL-MooseIDE-Tests/package.st b/src/FamixNGSQL-MooseIDE-Tests/package.st new file mode 100644 index 0000000..9744e1f --- /dev/null +++ b/src/FamixNGSQL-MooseIDE-Tests/package.st @@ -0,0 +1 @@ +Package { #name : #'FamixNGSQL-MooseIDE-Tests' } diff --git a/src/FamixNGSQL-MooseIDE/FmxSQLCoUsageExtractor.class.st b/src/FamixNGSQL-MooseIDE/FmxSQLCoUsageExtractor.class.st new file mode 100644 index 0000000..823c44b --- /dev/null +++ b/src/FamixNGSQL-MooseIDE/FmxSQLCoUsageExtractor.class.st @@ -0,0 +1,46 @@ +"Co-usage of database relations: containers are routines, views or tables; +inner boxes are referenced tables/views. Repeated references retain their frequency." +Class { + #name : #FmxSQLCoUsageExtractor, + #superclass : #MiCoInnerEntitiesAbstractExtractor, + #category : #'FamixNGSQL-MooseIDE' +} +{ #category : #accessing } +FmxSQLCoUsageExtractor class >> menuItemName [ ^ 'SQL relation references' ] +{ #category : #accessing } +FmxSQLCoUsageExtractor class >> description [ + ^ 'Referenced tables and views, including column and foreign-key references. Repeated references count as repeated uses.' +] +{ #category : #testing } +FmxSQLCoUsageExtractor class >> accepts: entity [ + ^ { FmxSQLTable. FmxSQLForeignTable. FmxSQLView. FmxSQLStoredProcedure } + anySatisfy: [ :class | entity isKindOf: class ] +] +{ #category : #opening } +FmxSQLCoUsageExtractor class >> openOn: entities [ + | browser | + (entities allSatisfy: [ :entity | self accepts: entity ]) ifFalse: [ + self error: 'SQL CoUsage expects only tables, views or routines' ]. + browser := MiCoUsageMapBrowser new. + browser specModel settings innerBoxExtractor: self. + browser open. + browser followEntity: entities asMooseGroup. + ^ browser +] +{ #category : #querying } +FmxSQLCoUsageExtractor >> innerEntitiesFor: entity [ + | relations | + (self class accepts: entity) ifFalse: [ self error: 'Unsupported SQL CoUsage entity' ]. + relations := OrderedCollection new. + entity queryAllOutgoing do: [ :reference | + ((reference isKindOf: FmxSQLColumnReference) or: [ + (reference isKindOf: FmxSQLTableReference) or: [ reference isKindOf: FmxSQLViewReference ] ]) ifTrue: [ + reference allTargets asSet do: [ :target | + | relation | + relation := (target isKindOf: FmxSQLColumn) + ifTrue: [ target columnsContainer ] ifFalse: [ target ]. + ((relation isKindOf: FmxSQLTable) or: [ + (relation isKindOf: FmxSQLForeignTable) or: [ relation isKindOf: FmxSQLView ] ]) + ifTrue: [ relations add: relation ] ] ] ]. + ^ relations +] diff --git a/src/FamixNGSQL-MooseIDE/FmxSQLDeadCodeRule.class.st b/src/FamixNGSQL-MooseIDE/FmxSQLDeadCodeRule.class.st new file mode 100644 index 0000000..a4ae8d4 --- /dev/null +++ b/src/FamixNGSQL-MooseIDE/FmxSQLDeadCodeRule.class.st @@ -0,0 +1,44 @@ +"SQL routines are externally callable. The default protects them all. +Opt into a closed-world candidate set only when its external callers are known. +Trigger/constraint entry points and incomplete analysis remain protected." +Class { + #name : #FmxSQLDeadCodeRule, + #superclass : #MiDeadAbstractRule, + #instVars : [ 'closedWorldCandidates', 'externalEntryPoints' ], + #category : #'FamixNGSQL-MooseIDE' +} +{ #category : #initialization } +FmxSQLDeadCodeRule >> initialize [ + super initialize. + closedWorldCandidates := IdentitySet new. + externalEntryPoints := IdentitySet new +] +{ #category : #accessing } +FmxSQLDeadCodeRule >> closedWorldCandidates: entities [ closedWorldCandidates := entities asIdentitySet ] +{ #category : #accessing } +FmxSQLDeadCodeRule >> externalEntryPoints: entities [ externalEntryPoints := entities asIdentitySet ] +{ #category : #accessing } +FmxSQLDeadCodeRule >> name [ ^ super name, 'PostgreSQL entry points and incomplete evidence' ] +{ #category : #testing } +FmxSQLDeadCodeRule >> selectedByDefault [ ^ false ] +{ #category : #testing } +FmxSQLDeadCodeRule >> notDead: entity [ + (entity isKindOf: FmxSQLStoredProcedure) ifFalse: [ ^ false ]. + (closedWorldCandidates includes: entity) ifFalse: [ ^ true ]. + (externalEntryPoints includes: entity) ifTrue: [ ^ true ]. + ((entity respondsTo: #triggers) and: [ entity triggers isNotEmpty ]) ifTrue: [ ^ true ]. + (entity checkConstraintsCalling isNotEmpty or: [ entity exclusionConstraintsCalling isNotEmpty ]) ifTrue: [ ^ true ]. + ^ (entity cacheAt: #analysisStatus ifAbsent: [ nil ]) ~= #visited +] + +{ #category : #opening } +FmxSQLDeadCodeRule class >> openOn: entities [ + | browser | + (entities isEmpty or: [ (entities allSatisfy: [ :entity | entity isKindOf: FmxSQLStoredProcedure ]) not ]) + ifTrue: [ self error: 'SQL dead-code analysis expects a nonempty routine collection' ]. + browser := MiDeadBrowser new. + browser open. + browser specModel chooseRules: { self new }. + browser followEntity: entities asMooseGroup. + ^ browser +] diff --git a/src/FamixNGSQL-MooseIDE/FmxSQLReplicationCleaner.class.st b/src/FamixNGSQL-MooseIDE/FmxSQLReplicationCleaner.class.st new file mode 100644 index 0000000..c057a0c --- /dev/null +++ b/src/FamixNGSQL-MooseIDE/FmxSQLReplicationCleaner.class.st @@ -0,0 +1,123 @@ +"PostgreSQL lexical cleaner. Preserves quoted text and line positions; assumes standard_conforming_strings = on." +Class { + #name : #FmxSQLReplicationCleaner, + #superclass : #FamixRepSourcesCleaner, + #instVars : [ 'source', 'index', 'output', 'state', 'depth', 'delimiter', 'escaped', 'pendingSpace', 'lineHasCode' ], + #category : #'FamixNGSQL-MooseIDE' +} + +{ #category : #accessing } +FmxSQLReplicationCleaner class >> description [ + ^ 'PostgreSQL comments and quoting (standard_conforming_strings=on)' +] + +{ #category : #printing } +FmxSQLReplicationCleaner class >> displayStringOn: stream [ + stream nextPutAll: 'PostgreSQL' +] + +{ #category : #opening } +FmxSQLReplicationCleaner class >> openOn: entities [ + | browser configuration | + (entities notEmpty and: [ entities allSatisfy: [ :entity | + (entity isKindOf: FmxSQLEntity) and: [ entity hasSourceAnchor ] ] ]) + ifFalse: [ self error: 'Select SQL entities with source anchors' ]. + browser := MiDuplicationBrowser new. + browser followEntity: entities asMooseGroup. + configuration := browser specModel currentConfiguration. + configuration sourcesCleaner: self new. + browser specModel updateFromConfiguration: configuration. + browser open. + ^ browser +] + +{ #category : #cleaning } +FmxSQLReplicationCleaner >> cleanedTextLines: text [ + | lines | + source := text. + index := 1. + output := String new writeStream. + state := #normal. + depth := 0. + pendingSpace := false. + lineHasCode := false. + [ index <= source size ] whileTrue: [ self scanNext ]. + (#(normal lineComment) includes: state) ifFalse: [ self error: 'Unterminated SQL quoted text or comment' ]. + lines := OrderedCollection new. + output contents lines doWithIndex: [ :line :lineNumber | + line ifNotEmpty: [ lines add: lineNumber -> line ] ]. + ^ lines +] + +{ #category : #private } +FmxSQLReplicationCleaner >> matches: text [ + ^ index + text size - 1 <= source size and: [ + (source copyFrom: index to: index + text size - 1) = text ] +] + +{ #category : #private } +FmxSQLReplicationCleaner >> putCode: character [ + (pendingSpace and: [ lineHasCode ]) ifTrue: [ output space ]. + pendingSpace := false. + output nextPut: character. + lineHasCode := true +] + +{ #category : #private } +FmxSQLReplicationCleaner >> dollarDelimiter [ + | end tag | + end := source indexOf: $$ startingAt: index + 1 ifAbsent: [ 0 ]. + end = 0 ifTrue: [ ^ nil ]. + tag := source copyFrom: index + 1 to: end - 1. + (tag notEmpty and: [ (tag first isLetter or: [ tag first = $_ ]) not + or: [ (tag allSatisfy: [ :c | c isAlphaNumeric or: [ c = $_ ] ]) not ] ]) ifTrue: [ ^ nil ]. + (index > 1 and: [ (source at: index - 1) isAlphaNumeric or: [ (source at: index - 1) = $_ ] ]) ifTrue: [ ^ nil ]. + ^ source copyFrom: index to: end +] + +{ #category : #private } +FmxSQLReplicationCleaner >> scanNext [ + | c quote | + c := source at: index. + state = #dollar ifTrue: [ + (self matches: delimiter) + ifTrue: [ output nextPutAll: delimiter. index := index + delimiter size. state := #normal ] + ifFalse: [ output nextPut: c. index := index + 1 ]. + ^ self ]. + state = #quoted ifTrue: [ + output nextPut: c. + index := index + 1. + (escaped and: [ c = $\ and: [ index <= source size ] ]) ifTrue: [ + output nextPut: (source at: index). index := index + 1. ^ self ]. + c = delimiter ifTrue: [ + (index <= source size and: [ (source at: index) = delimiter ]) + ifTrue: [ output nextPut: delimiter. index := index + 1 ] + ifFalse: [ state := #normal ] ]. + ^ self ]. + (c = Character cr or: [ c = Character lf ]) ifTrue: [ + output nextPut: c. pendingSpace := false. lineHasCode := false. + state = #lineComment ifTrue: [ state := #normal ]. + index := index + 1. ^ self ]. + state = #lineComment ifTrue: [ index := index + 1. ^ self ]. + state = #blockComment ifTrue: [ + (self matches: '/*') ifTrue: [ depth := depth + 1. index := index + 2. ^ self ]. + (self matches: '*/') ifTrue: [ depth := depth - 1. index := index + 2. + depth = 0 ifTrue: [ state := #normal ]. ^ self ]. + index := index + 1. ^ self ]. + c isSeparator ifTrue: [ pendingSpace := true. index := index + 1. ^ self ]. + (self matches: '--') ifTrue: [ state := #lineComment. index := index + 2. ^ self ]. + (self matches: '/*') ifTrue: [ state := #blockComment. depth := 1. + pendingSpace := true. index := index + 2. ^ self ]. + quote := c = $' or: [ c = $" ]. + quote ifTrue: [ + escaped := c = $' and: [ index > 1 and: [ + (source at: index - 1) asLowercase = $e and: [ index = 2 or: [ + ((source at: index - 2) isAlphaNumeric or: [ '_$' includes: (source at: index - 2) ]) not ] ] ] ]. + delimiter := c. state := #quoted ]. + (c = $$ and: [ (delimiter := self dollarDelimiter) notNil ]) ifTrue: [ + self putCode: c. + output nextPutAll: delimiter allButFirst. + index := index + delimiter size. state := #dollar. ^ self ]. + self putCode: c. + index := index + 1 +] diff --git a/src/FamixNGSQL-MooseIDE/FmxSQLRoassalViews.class.st b/src/FamixNGSQL-MooseIDE/FmxSQLRoassalViews.class.st new file mode 100644 index 0000000..f13d7bc --- /dev/null +++ b/src/FamixNGSQL-MooseIDE/FmxSQLRoassalViews.class.st @@ -0,0 +1,78 @@ +"Roassal 3 views using SQL containment and association APIs. Builders return unopened canvases." +Class { + #name : #FmxSQLRoassalViews, + #superclass : #Object, + #category : #'FamixNGSQL-MooseIDE' +} +{ #category : #building } +FmxSQLRoassalViews class >> schemaTablesIn: model [ + | graph | + graph := RSMondrian new. + graph shape box color: Color veryLightGray; labeled: #name. + graph nodes: (model allWithType: FmxSQLNamespace) forEach: [ :schema | + | tables | + tables := schema namespaceEntities select: [ :e | e isKindOf: FmxSQLTable ]. + graph shape box color: Color blue lighter; labeled: #name. + graph nodes: tables. + graph layout grid ]. + graph layout grid. + graph build. + ^ graph canvas +] +{ #category : #building } +FmxSQLRoassalViews class >> foreignKeysIn: model [ + ^ self graphOf: (model allWithType: FmxSQLTable) pairs: (self foreignKeyPairsIn: model) +] +{ #category : #building } +FmxSQLRoassalViews class >> routineCallsIn: model [ + ^ self graphOf: (self routinesIn: model) pairs: (self routineCallPairsIn: model) +] +{ #category : #private } +FmxSQLRoassalViews class >> graphOf: entities pairs: pairs [ + | graph | + graph := RSMondrian new. + graph shape box color: Color green muchLighter; labeled: [ :entity | self labelFor: entity ]. + graph nodes: entities. + graph arrowedLine color: Color gray; connectToAll: [ :entity | + (pairs select: [ :pair | pair first == entity ]) collect: #second ]. + graph layout circle. + graph build. + graph canvas edges do: #pushFront. + ^ graph canvas +] +{ #category : #private } +FmxSQLRoassalViews class >> labelFor: entity [ + ^ (entity parentNamespace ifNil: [ '' ] ifNotNil: [ :ns | ns name, '.' ]), entity name +] +{ #category : #extracting } +FmxSQLRoassalViews class >> foreignKeyPairsIn: model [ + | tables pairs | + tables := IdentitySet withAll: (model allWithType: FmxSQLTable). + pairs := Set new. + (model allWithType: FmxSQLForeignKeyConstraint) do: [ :foreignKey | + foreignKey outgoingForeignKeyColumnReferences do: [ :reference | + | target | + target := reference target ifNotNil: [ :column | column columnsContainer ]. + ((tables includes: foreignKey table) and: [ tables includes: target ]) ifTrue: [ + pairs add: { foreignKey table. target } ] ] ]. + ^ pairs +] +{ #category : #extracting } +FmxSQLRoassalViews class >> routinesIn: model [ + ^ ((model allWithType: FmxSQLStoredProcedure) asOrderedCollection + addAll: (model allWithType: FmxSQLTriggerStoredProcedure); yourself) asIdentitySet +] +{ #category : #extracting } +FmxSQLRoassalViews class >> routineCallPairsIn: model [ + | routines pairs | + routines := self routinesIn: model. + pairs := Set new. + (model allWithType: FmxSQLStoredProcedureCall) do: [ :call | + | caller | + caller := call source. + (caller notNil and: [ (routines includes: caller) not ]) ifTrue: [ + caller := caller allParents detect: [ :parent | routines includes: parent ] ifNone: [ nil ] ]. + (routines includes: caller) ifTrue: [ + call allTargets do: [ :target | (routines includes: target) ifTrue: [ pairs add: { caller. target } ] ] ] ]. + ^ pairs +] diff --git a/src/FamixNGSQL-MooseIDE/FmxSQLSourceTextAdapter.class.st b/src/FamixNGSQL-MooseIDE/FmxSQLSourceTextAdapter.class.st new file mode 100644 index 0000000..3ab9663 --- /dev/null +++ b/src/FamixNGSQL-MooseIDE/FmxSQLSourceTextAdapter.class.st @@ -0,0 +1,34 @@ +"Adapts PostgreSQL definitions and owner-relative AST intervals without file anchors." +Class { + #name : #FmxSQLSourceTextAdapter, + #superclass : #MiAbstractSourceTextAnchorAdapter, + #instVars : [ 'sourceOwner', 'offset' ], + #category : #'FamixNGSQL-MooseIDE' +} +{ #category : #testing } +FmxSQLSourceTextAdapter class >> acceptEntity: entity [ + ^ (entity isKindOf: FmxSQLEntity) and: [ entity respondsTo: #sourceText ] +] +{ #category : #initialization } +FmxSQLSourceTextAdapter >> initializeDisplayedText: text andEntity: entity [ + | anchor | + super initializeDisplayedText: text andEntity: entity. + sourceOwner := entity. + offset := 0. + anchor := entity sourceAnchor. + ((anchor isKindOf: FmxSQLEntitySourceAnchor) and: [ anchor hasSourceText ]) ifTrue: [ + sourceOwner := anchor entity. + offset := anchor start - 1 ] +] +{ #category : #accessing } +FmxSQLSourceTextAdapter >> intervalForEntity: entity [ + | anchor start end | + (entity respondsTo: #sourceAnchor) ifFalse: [ ^ nil ]. + anchor := entity sourceAnchor. + ((anchor isKindOf: FmxSQLEntitySourceAnchor) and: [ anchor hasSourceText ]) ifFalse: [ ^ nil ]. + anchor entity == sourceOwner ifFalse: [ ^ nil ]. + start := anchor start - offset. + end := anchor end - offset. + (start > 0 and: [ end >= start and: [ end <= displayedText size ] ]) ifFalse: [ ^ nil ]. + ^ start to: end +] diff --git a/src/FamixNGSQL-MooseIDE/package.st b/src/FamixNGSQL-MooseIDE/package.st new file mode 100644 index 0000000..cba7d61 --- /dev/null +++ b/src/FamixNGSQL-MooseIDE/package.st @@ -0,0 +1 @@ +Package { #name : #'FamixNGSQL-MooseIDE' }