001package gudusoft.gsqlparser.sqlenv;
002
003import gudusoft.gsqlparser.nodes.TTypeName;
004import gudusoft.gsqlparser.util.SQLUtil;
005
006import java.util.*;
007
008public abstract class TSQLRoutine extends TSQLSchemaObject{
009    // volatile: written during registration, read lock-free by other threads
010    private volatile String definition;
011
012    public void setDefinition(String definition) {
013        this.definition = definition;
014    }
015
016    public String getDefinition() {
017        return definition;
018    }
019
020    public TSQLRoutine(TSQLSchema schema, String objectName, ESQLDataObjectType dataObjectType){
021        super(schema,objectName,dataObjectType);
022    }
023
024    /**
025     * column list
026     * @return a column list
027     */
028    public List<TSQLParameter> getParameterList() {
029        List<TSQLParameter> parameterList = new LinkedList<>();
030        Map<String, TSQLParameter> m = getParameterMap();
031        synchronized (m) {
032            for(String parameter: m.keySet()){
033                parameterList.add(m.get(parameter));
034            }
035        }
036        return parameterList;
037    }
038
039
040    // volatile + double-checked lazy init: concurrent analyzers registering the
041    // same routine share this instance, so the map must be published safely
042    private volatile Map<String, TSQLParameter> parameterMap;
043
044    private Map<String, TSQLParameter> getParameterMap() {
045        Map<String, TSQLParameter> m = parameterMap;
046        if (m == null) {
047            synchronized (this) {
048                if (parameterMap == null) {
049                    parameterMap = Collections.synchronizedMap(new LinkedHashMap<String, TSQLParameter>());
050                }
051                m = parameterMap;
052            }
053        }
054        return m;
055    }
056
057
058    public void addParameter(int index, TTypeName parameterDataType){
059        Map<String, TSQLParameter> m = getParameterMap();
060        // search-then-put must be atomic under concurrent registration
061        synchronized (m) {
062            if (!searchParameter(index,parameterDataType,null)){
063                m.put(SQLUtil.getIdentifierNormalColumnName(this.sqlEnv.getDBVendor(), makeParameterMapId(index,parameterDataType,null)), new TSQLParameter(this, index,parameterDataType,null));
064            }
065        }
066    }
067
068    public void addParameter(int index, TTypeName parameterDataType,String parameterName){
069        Map<String, TSQLParameter> m = getParameterMap();
070        // search-then-put must be atomic under concurrent registration
071        synchronized (m) {
072            if (!searchParameter(index,parameterDataType,parameterName)){
073                m.put(SQLUtil.getIdentifierNormalColumnName(this.sqlEnv.getDBVendor(), makeParameterMapId(index,parameterDataType,parameterName)), new TSQLParameter(this,index,parameterDataType,parameterName));
074            }
075        }
076    }
077
078
079    public boolean searchParameter(int index, TTypeName datatype, String parameterName){
080        return getParameterMap().containsKey(SQLUtil.getIdentifierNormalColumnName(this.sqlEnv.getDBVendor()
081                , makeParameterMapId(index,datatype,parameterName)));
082    }
083
084    public TSQLParameter getParameter(int index, TTypeName datatype, String parameterName){
085        return getParameterMap().get(SQLUtil.getIdentifierNormalColumnName(this.sqlEnv.getDBVendor()
086                , makeParameterMapId(index,datatype,parameterName)));
087    }
088
089    String makeParameterMapId(int index, TTypeName datatype, String parameterName){
090        return index+datatype.toString()+parameterName == null?"N/A":parameterName;
091    }
092
093    /**
094     * A value set in advance for a parameter of the routine (Mantis 4962): the dynamic SQL evaluation of the routine
095     * reads it as the parameter's value, ahead of the routine's call sites, as a caller passing it would. A
096     * {@link Kind#SUPPLIED_UNKNOWN} value says a caller passes a value nobody knows: the parameter is unknown, not
097     * absent, so its default does not apply.
098     */
099    public static final class PresetValue {
100        public enum Kind { STRING, NUMBER, SQL_NULL, SUPPLIED_UNKNOWN }
101
102        private final Kind kind;
103        private final String text;
104
105        private PresetValue(Kind kind, String text) {
106            this.kind = kind;
107            this.text = text;
108        }
109
110        public static PresetValue string(String value) {
111            return value == null ? sqlNull() : new PresetValue(Kind.STRING, value);
112        }
113
114        /** A number, as written ({@code "5"}, {@code "1.5"}). */
115        public static PresetValue number(String text) {
116            if (text == null) {
117                return sqlNull();
118            }
119            new java.math.BigDecimal(text.trim()); // a number or an IllegalArgumentException
120            return new PresetValue(Kind.NUMBER, text.trim());
121        }
122
123        public static PresetValue sqlNull() {
124            return new PresetValue(Kind.SQL_NULL, null);
125        }
126
127        public static PresetValue suppliedUnknown() {
128            return new PresetValue(Kind.SUPPLIED_UNKNOWN, null);
129        }
130
131        public Kind getKind() {
132            return kind;
133        }
134
135        /** The string or the number as written; null for SQL NULL and for a supplied unknown value. */
136        public String getText() {
137            return text;
138        }
139
140        @Override
141        public String toString() {
142            switch (kind) {
143                case STRING:
144                    return "'" + text + "'";
145                case NUMBER:
146                    return text;
147                case SQL_NULL:
148                    return "NULL";
149                default:
150                    return "(supplied, not known)";
151            }
152        }
153    }
154
155    /** The presets by the parameter's identity, each with the name it was set under; built on first use. */
156    private volatile Map<CanonKey, Map.Entry<String, PresetValue>> parameterPresets;
157
158    private Map<CanonKey, Map.Entry<String, PresetValue>> presets() {
159        Map<CanonKey, Map.Entry<String, PresetValue>> m = parameterPresets;
160        if (m == null) {
161            synchronized (this) {
162                if (parameterPresets == null) {
163                    parameterPresets = Collections.synchronizedMap(
164                            new LinkedHashMap<CanonKey, Map.Entry<String, PresetValue>>());
165                }
166                m = parameterPresets;
167            }
168        }
169        return m;
170    }
171
172    /** The parameter's identity under the catalog's own identifier rules (its configured profile). */
173    private CanonKey presetKey(String parameterName) {
174        return this.sqlEnv.getIdentifierService().canonKey(parameterName, ESQLDataObjectType.dotColumn);
175    }
176
177    /**
178     * Sets, or with a null value removes, the preset of the parameter named {@code parameterName}, as the procedure
179     * declares it ({@code @pipeline_type}); the name is compared as the vendor compares column names.
180     */
181    public void setParameterPreset(String parameterName, PresetValue value) {
182        if (parameterName == null) {
183            return;
184        }
185        if (value == null) {
186            if (parameterPresets != null) {
187                parameterPresets.remove(presetKey(parameterName));
188            }
189        } else {
190            presets().put(presetKey(parameterName),
191                    new AbstractMap.SimpleImmutableEntry<String, PresetValue>(parameterName, value));
192            // the catalog finds a routine with presets without a lookup that could fetch metadata
193            this.sqlEnv.registerPresetRoutine(this);
194        }
195    }
196
197    /** The preset of the parameter named {@code parameterName}, or null when none is set. */
198    public PresetValue getParameterPreset(String parameterName) {
199        Map<CanonKey, Map.Entry<String, PresetValue>> m = parameterPresets;
200        Map.Entry<String, PresetValue> entry = parameterName == null || m == null ? null
201                : m.get(presetKey(parameterName));
202        return entry == null ? null : entry.getValue();
203    }
204
205    /** Every preset, by the name it was set under, in the order set. */
206    public Map<String, PresetValue> getParameterPresets() {
207        Map<String, PresetValue> presets = new LinkedHashMap<String, PresetValue>();
208        Map<CanonKey, Map.Entry<String, PresetValue>> m = parameterPresets;
209        if (m == null) {
210            return presets;
211        }
212        synchronized (m) {
213            for (Map.Entry<String, PresetValue> entry : m.values()) {
214                presets.put(entry.getKey(), entry.getValue());
215            }
216        }
217        return presets;
218    }
219
220    private static final java.util.logging.Logger LOG = java.util.logging.Logger.getLogger(TSQLRoutine.class.getName());
221
222    /**
223     * Sets the presets a metadata file gives the routine ({@code procedures[].parameterBindings}): each entry a
224     * parameter name and a JSON value, a string, a number, or null for SQL NULL. A value of another kind (true,
225     * false, an object, an array) is not read and is logged with {@code origin}. A later file's preset of a parameter
226     * replaces an earlier one's.
227     */
228    public void applyParameterBindings(Map<?, ?> bindings, String origin) {
229        if (bindings == null) {
230            return;
231        }
232        for (Map.Entry<?, ?> entry : bindings.entrySet()) {
233            if (!(entry.getKey() instanceof String)) {
234                continue;
235            }
236            String parameter = (String) entry.getKey();
237            Object value = entry.getValue();
238            PresetValue preset;
239            try {
240                if (value == null) {
241                    preset = PresetValue.sqlNull();
242                } else if (value instanceof String) {
243                    preset = PresetValue.string((String) value);
244                } else if (value instanceof Number) {
245                    preset = PresetValue.number(numberText((Number) value));
246                } else {
247                    LOG.warning("parameterBindings of " + getName() + (origin == null ? "" : " in " + origin) + ": "
248                            + parameter + " has a value that is not a string, a number or null; it is not read");
249                    continue;
250                }
251            } catch (RuntimeException e) {
252                // one value the catalog cannot hold (a number out of range): that binding only is skipped
253                LOG.warning("parameterBindings of " + getName() + (origin == null ? "" : " in " + origin) + ": "
254                        + parameter + " = " + value + " is not read: " + e.getMessage());
255                continue;
256            }
257            replacePreset(parameter, preset, origin);
258        }
259    }
260
261    /** Takes over {@code from}'s presets; one it gives for a parameter replaces this routine's, logged. */
262    public void mergeParameterPresets(TSQLRoutine from, String origin) {
263        if (from == null || from == this) {
264            return;
265        }
266        for (Map.Entry<String, PresetValue> preset : from.getParameterPresets().entrySet()) {
267            replacePreset(preset.getKey(), preset.getValue(), origin);
268        }
269    }
270
271    private void replacePreset(String parameter, PresetValue preset, String origin) {
272        PresetValue earlier = getParameterPreset(parameter);
273        if (earlier != null && (earlier.getKind() != preset.getKind()
274                || !String.valueOf(earlier.getText()).equals(String.valueOf(preset.getText())))) {
275            LOG.info("preset of " + parameter + " of " + getName() + (origin == null ? "" : " from " + origin) + ": "
276                    + preset + " replaces " + earlier);
277        }
278        setParameterPreset(parameter, preset);
279    }
280
281    /**
282     * A JSON number's text: exact for the integers and long decimals the parser keeps exact ({@code BigInteger},
283     * {@code BigDecimal}, {@code Long}); for a shorter decimal the parser reads as a double, the shortest text of
284     * that double ({@code 5.0} stays {@code 5.0}). A value no decimal holds (an infinite double) is an error.
285     */
286    private static String numberText(Number n) {
287        if (n instanceof java.math.BigDecimal) {
288            return ((java.math.BigDecimal) n).toPlainString();
289        }
290        if (n instanceof Double || n instanceof Float) {
291            return java.math.BigDecimal.valueOf(n.doubleValue()).toPlainString();
292        }
293        return n.toString();
294    }
295
296
297}